Recording HTTP requests with WebMock
TweetFor the past few weeks our team has been working on auto-generating our API documentation. As part of this we needed a way to record external HTTP requests in order to show request examples.
Since most of our integration tests use Webmock for external request stubbing we just needed a way to tie into Webmock to log our request stubs.
Recording with Webmock
It turns out Webmock provides an API call to do just that. With webmock you can register a callback to log requests.
Webmock has an after_request
class method you can use for logging request data.
For example, inside a test case we can register a callback:
it "makes an external request" do
WebMock.after_request do |req, response|
request = {
uri: req.uri.to_s,
method: req.method.to_s.upcase,
headers: req.headers,
body: req.body
}
puts request.inspect
end
stub_request(:post, "http://example.com/callback")
uri = URI.parse("http://example.com/callback")
Net::HTTP.post_form(uri, {"payload" => "data"})
# clear all registered callbacks to prevent
# our callback from running in other tests
WebMock::CallbackRegistry.reset
end
This example works, but its not really clean. It would be nicer to extract our own class here for request recording.
Looking at Webmock’s callback registry class
we can see each callback responds to #call(request, response)
. Let’s write
a recorder class to write request data to a JSON file somewhere:
# lib/recorder.rb
module Recorder
class Writer
def initialize(spec_name)
@spec_name = spec_name
end
def call(req, resp)
puts "Recording #{@spec_name}"
request = {
uri: req.uri.to_s,
method: req.method.to_s.upcase,
headers: req.headers,
body: req.body
}.to_json
File.write(File.join("#{@spec_name}.json"), request)
end
end
end
# spec/some_test_spec.rb
it "makes an external request" do |example|
# setup the callback
spec_name = example.description.gsub(/\W/, '_')
callback = Recorder::Writer.new(spec_name)
WebMock::CallbackRegistry.add_callback({}, callback)
# make external request
stub_request(:post, "http://example.com/callback")
uri = URI.parse("http://example.com/callback")
Net::HTTP.post_form(uri, {"payload" => "data"})
# clear all registered callbacks to prevent
# our callback from running in other tests
WebMock::CallbackRegistry.reset
end
With the above example we can now write request data from tests to a JSON file. However, our test case isn’t looking any cleaner yet. Let’s DRY things up a little using an around hook.
RSpec Around Hooks
RSpec has a neat feature called around hooks. Using an around hook we can register our callback only when a test spec is tagged with a specific meta tag.
The following example will only run when the :record
tag is passed to RSpec:
# spec/support/recorder.rb
RSpec.configure do |config|
config.around(:each, :record) do |example|
# setup the callback
spec_name = example.description.gsub(/\W/, '_')
WebMock::CallbackRegistry.add_callback({}, Recorder::Writer.new(spec_name))
# run the test case
example.run
# clear any webmock callbacks to prevent conflicts
# with other tests
WebMock::CallbackRegistry.reset
end if ENV['RECORD']
end
# spec/some_test_spec.rb
it "makes an external request", :record do
stub_request(:post, "http://example.com/callback")
uri = URI.parse("http://example.com/callback")
Net::HTTP.post_form(uri, {"payload" => "data"})
end
Note, the use of ENV['RECORD']
in our around hook. This allows us to only record tests when this environment
variable is present.
In order to record we will need to tell RSpec to run tests
tagged with :record
.
RECORD=true bundle exec rspec --tag record
Finally, you can use the above command whenever you want to generate up-to-date examples. It’s a good idea to add this to your deployment pipeline to generate updated examples everytime you push new code.
Hopefully this is enough to get you started with request recording. If you have any questions or better ways to improve upon this please feel free to comment.