By

Recording HTTP requests with WebMock

For 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.

Looking for a Rails developer
Degica is looking for a Rails developer to work with our dev team. Checkout the link for more information.