Configure VCR with RSpec
Tags: vcr, rspec, ruby, rails
Published: 2022-07-23 21:55:17

---
title: Configure VCR with RSpec
published: true
description: Configuring the VCR to allow fine-grained configuration for each spec.
tags: vcr, rspec, ruby, rails
//cover_image: https://direct_url_to_image.jpg
---

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](https://github.com/bblimke/webmock) which allow easily mock the http responses for your requests.

```ruby
# spec/webmock_spec.rb

stub_request(:get, 'https://example.com/service').
  to_return(status: 200 body: '{"a": "abc"}')
```
> 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:

```ruby
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
```
> 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](https://github.com/vcr/vcr).


# 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:


```ruby
# 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
```

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](https://relishapp.com/vcr/vcr/v/6-1-0/docs/test-frameworks/usage-with-rspec-metadata). 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.

```ruby
require 'vcr'

VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr_cassettes'
  c.hook_into :webmock
end
```

### 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`:

```ruby
# 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
```

### Configure VCR's shared_context

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

```ruby
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
```

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

```ruby
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

```

#### 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:

```ruby
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
```

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


```ruby
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
```


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