MiniMock
A Ruby Gem to record and replay requests using Typhoeus’ built-in stubbing functionality.
Installation
Add gem "mini_mock"
to your Gemfile or install it yourself with gem install mini_mock
.
Usage
#Start recording all Tyhpoeus requests
MiniMock.record
#Get a quote from GitHub's zen api
response = Typhoeus.get 'https://api.github.com/zen'
puts response.body
# => Avoid administrative distraction.
#Load saved mocks and block all outgoing requests
MiniMock.replay
#No new request is made
response = Typhoeus.get 'https://api.github.com/zen'
puts response.body
# => Avoid administrative distraction.
MiniMock saves responses in a ruby file with calls to Typhoeus.stub so that they can be easly edited to test how your code handles unexpected responses, errors, timeouts and all kinds of trickery the developers of the api you are consuming might have left for you to deal with.
Here is the file generated by the example above:
# mini_mock/mocks.rb
response = Typhoeus::Response.new(
code: 200,
status_message: "",
body: "Avoid administrative distraction.",
headers: {"server"=>"GitHub.com", "date"=>"Mon, 13 Mar 2023 23:20:38 GMT", "content-type"=>"text/plain;charset=utf-8", "x-github-api-version-selected"=>"2022-11-28", "access-control-expose-headers"=>"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset", "access-control-allow-origin"=>"*", "strict-transport-security"=>"max-age=31536000; includeSubdomains; preload", "x-frame-options"=>"deny", "x-content-type-options"=>"nosniff", "x-xss-protection"=>"0", "referrer-policy"=>"origin-when-cross-origin, strict-origin-when-cross-origin", "content-security-policy"=>"default-src 'none'", "vary"=>"Accept-Encoding, Accept, X-Requested-With", "x-ratelimit-limit"=>"60", "x-ratelimit-remaining"=>"40", "x-ratelimit-reset"=>"1678753155", "x-ratelimit-resource"=>"core", "x-ratelimit-used"=>"20", "accept-ranges"=>"bytes", "content-length"=>"33", "x-github-request-id"=>"C559:5344:3E4851:477975:640FAFC6"},
effective_url: "https://api.github.com/zen",
options: {:httpauth_avail=>0, :total_time=>0.240721, :starttransfer_time=>0.239971, :appconnect_time=>0.078561, :pretransfer_time=>0.078669, :connect_time=>0.042008, :namelookup_time=>0.017529, :redirect_time=>0.0, :effective_url=>"https://api.github.com/zen", :primary_ip=>"20.201.28.148", :response_code=>200, :request_size=>115, :redirect_count=>0, :redirect_url=>nil, :size_upload=>0.0, :size_download=>33.0, :speed_upload=>0.0, :speed_download=>137.0, :return_code=>:ok, :response_headers=>"HTTP/2 200 \r\nserver: GitHub.com\r\ndate: Mon, 13 Mar 2023 23:20:38 GMT\r\ncontent-type: text/plain;charset=utf-8\r\nx-github-api-version-selected: 2022-11-28\r\naccess-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset\r\naccess-control-allow-origin: *\r\nstrict-transport-security: max-age=31536000; includeSubdomains; preload\r\nx-frame-options: deny\r\nx-content-type-options: nosniff\r\nx-xss-protection: 0\r\nreferrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin\r\ncontent-security-policy: default-src 'none'\r\nvary: Accept-Encoding, Accept, X-Requested-With\r\nx-ratelimit-limit: 60\r\nx-ratelimit-remaining: 40\r\nx-ratelimit-reset: 1678753155\r\nx-ratelimit-resource: core\r\nx-ratelimit-used: 20\r\naccept-ranges: bytes\r\ncontent-length: 33\r\nx-github-request-id: C559:5344:3E4851:477975:640FAFC6\r\n\r\n", :response_body=>"Avoid administrative distraction.", :debug_info=>""}
)
Typhoeus.stub("https://api.github.com/zen", {:method=>:get})
.and_return(response)
The line lengths are not great, but that's a tradeoff to make the mocks file easier to navigate while keeping complete responses and requests in the same file. Speaking of messy mocks files, mocks.rb is just the default file name. To organize your mocks in multiple files, pass a parameter to .record
as in MiniMock.record('open-ai-api')
and subsequent responses will be saved to open-ai-api.rb in the mini_mock folder. To latter load these responses, call MiniMock.replay('open-ai-api')
.
This is also useful in tests:
Run this test once as
test "make some requests" do
MiniMock.record 'reqs_test'
# your job/hydra run
# test assertions
end
And then forever, deterministically, as
test "make some requests" do
MiniMock.replay 'reqs_test'
# your job/hydra run
# test assertions
end
To turn off MiniMock and allow real network connections again, use the .off
method:
MiniMock.off
This will also clear any currenly loaded mocks.
Weirdness
Saving requests to ruby files from within a heredoc is, to say the least, uncoventional. While this gem is desinged to be fun to use in development/testing, it's implementation probalby has issues with more complex requests. Writting stuff from the internet to a file that will be latter executed as Ruby also may need more security consideration than just calling inspect
on the response values. That's all probalby fine if you are having fun and working with somewhat trusted data. Otherwise, checkout more advanced and safer mocking gems like WebMock and VCR.
Considerations
- MiniMock only supports Typhoeus as the HTTP library, while other gems support multiple HTTP libraries such as Net::HTTP, RestClient, Faraday, etc.
- MiniMock only records requests and responses as they are, without allowing any automatic filtering of sensitive data such as passwords or API keys.
- MiniMock does not provide any way to verify that the expected requests have been made, or to set expectations on the number or order of requests.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/felipedmesquita/mini_mock.