DEV Community

Jorge fernando alves feitoza
Jorge fernando alves feitoza

Posted on • Updated on

Configure VCR with RSpec

In development, soon or later we need to do some integration with a third-party service and usually this means perform http requests.

As a good developer, we'll add specs in our codebase to make sure that everything is well tested.

In order to keep our test suite faster and consistent, we need to mock our http requests. A simple and good way to achieve this is using the Webmock gem which allow easily mock the http responses for your requests.

# spec/webmock_spec.rb

stub_request(:get, 'https://example.com/service').
  to_return(status: 200 body: '{"a": "abc"}')
Enter fullscreen mode Exit fullscreen mode

Example of mock using Webmock

And this works well, but if we need perform many requests to accomplish a task or business logic, just mocking become complicated and hard to maintain.

Example: Lets imagine that we need import some products in our system, but these products are separeted in different categories and to get the products we need get the all the categories, iterate over them to retrieve the products:

let(:categories_endpoint) { "https://example.com/categories" }

let(:categories_response) do
  [{id: 'category-one-id'}, {id: 'category-two-id', name: ''}].to_json
end

let(:first_category_endpoint) { "#{categories_endpoint}/category-one-id" }
let(:first_category_response) do
  {
    products_links: [
      {link: 'https://example.com/categories/categor-one-id/product-one'}
    ]
  }.to_json
end

let(:second_category_endpoint) { "#{categories_endpoint}/category-two-id" }
let(:second_category_response) do
  {
    products_links: [
      {link: 'https://example.com/categories/categor-two-id/other-product'}
    ]
  }.to_json
end

let(:first_product_endpoint) { 'https://example.com/categories/categor-one-id/product-one' }
let(:first_product_response) { read_fixture('fixtures/products/first_product.json') }
let(:second_product_endpoint) { 'https://example.com/categories/categor-two-id/other-product' }
let(:second_product_response) { read_fixture('fixtures/products/second_product.json') }


before do
  stub_request(:get, categories_endpoint).to_return(status: 200, body: categories_response)

  stub_request(:get, first_category_endpoint).to_return(status: 200, body: first_category_response)
  stub_request(:get, second_category_endpoint).to_return(status: 200, body: second_category_response)

  stub_request(:get, first_product_endpoint).to_return(status: 200, body: first_product_response)
  stub_request(:get, second_product_endpoint).to_return(status: 200, body: second_product_response)
end
Enter fullscreen mode Exit fullscreen mode

A possible mock for the example

As we can see the configurations to mock is huge, and if we need add a thirdy category, these configs will increase even more, becoming hard to read and maintain.
Another problem is that we are spending a time creating that could be use in other places in our application.

A way to avoid this is using the VCR gem.

The VCR gem

The VCR is a gem that record http interactions in our test suite and allow replay these interactions in future runs.

VCR usage

The VCR usage is pretty simple, as follows:

# Configure it

VCR.configure do |config|
  config.cassette_library_dir = "fixtures/vcr_cassettes"
  config.hook_into :webmock
end

# Uses it

it 'does something' do
  VCR.use_cassette('my_cassete') do
    expect(do_request.body).to eql({success: true}.to_json)
  end
end
Enter fullscreen mode Exit fullscreen mode

The configuration is simple but the usage is a bit annoing, requiring us wrap all the HTTP interactions in blocks, its also break the TDD "configure, execute and assert" pattern.

VCR with RSpec

The VCR provides an integration with RSpec that uses the RSpec's metadata to configure itself. The configuration required to use this feature is here. But the idea of this article is have fine-grained control over VCR, so we'll not use it.

Fine-grained control of VCR with RSpec

Let's configure the VCR with RSpec to give to us a fine-grained control for us. The configuration follow these steps:

  • VCR regular configuration
  • Configure VCR to don't run on specs which its not required
  • Configure the VCR using RSpec shared_contexts

VCR regular configuration

Just the regular configuration, as follows the VCR docs.

require 'vcr'

VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr_cassettes'
  c.hook_into :webmock
end
Enter fullscreen mode Exit fullscreen mode

Disable VCR in specs which its not required

This is not exactly required, but its a good practice, also will make our tests fail if they perform some HTTP requests or use regular mocks without problems.

The VCR has an method turned_off that accepts a block of code which to be executed without the VCR. So to disable the VCR on specs which its not required, we'll use the RSpec hook around:

# specs/spec_helper.rb

RSpec.configure do |config|
  config.around do |example|

    # Just disable the VCR, the configuration for its usage
    # will be done in a shared_context
    if example.metadata[:vcr]
      example.run
    else
      VCR.turned_off { example.run }
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Configure VCR's shared_context

The RSpec's shared_context will allow us enable the VCR only when we need:

shared_context 'with vcr', vcr: true do
  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name) do
      example.run
    end

    VCR.turn_off!
  end
end
Enter fullscreen mode Exit fullscreen mode

With this shared_context we can use it as follow and the http will be recorded:

describe 'using vcr', vcr: true do
  # Configure the cassete name
  let(:cassete_name) { 'path/to/the/interaction' }

  it 'record the http interaction' do
    expect(do_request.body).to eql({ success: true }.to_json)
  end

  it 'reuse the same cassete here' do
    expect(do_request.headers).to include('x-custom-header' => 'abc')
  end
end

Enter fullscreen mode Exit fullscreen mode

Improving shared_context

The VCR use_cassete method accepts many other options, like the record_mode for example. Using the shared_context and let allow us configure the VCR to record new interactions in development but raise an error on CI, for example:

shared_context 'with vcr', vcr: true do
  # Disable new records on CI. Most of the CI providers
  # configure environment variable called CI.
  let(:cassette_record) { ENV['CI'] ? :none : :new_episodes }

  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name, { record: cassette_record }) do
      example.run
    end

    VCR.turn_off!
  end
end
Enter fullscreen mode Exit fullscreen mode

Creating specific shared_contexts

It is possible create specific shared_context that configure the VCR for specific cases. For example, imagine that you need not ignore the headers for some specific requests.

shared_context 'with vcr matching headers', vcr_matching_headers: true do
  around do |example|
    VCR.turn_on!

    VCR.use_cassette(cassette_name, { match_requests_on: [:method, :uri, :headers]}) do
      example.run
    end

    VCR.turn_off!
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are more options on VCR that we could add, but the examples gives the idea about how to control the VCR in our test suite.

Top comments (0)