TIL about VCR

Martijn

Martijn Versluis – 4 November 2019
735 words in about 3 minutes

A while ago I contributed a change to the well-known Ruby gem VCR. You can read all about it in this pull request, but basically it allows you to prevent interactions from being recorded until your test passes.

To be able to make this change, I obviously had to dive into (a portion of) the source code to understand what I had to change. By reading the code I discovered some VCR features I did not know, so they might be new to you too. Here are six things you might not know about VCR.

1. You can change the serialization

VCR is well known for the YAML files it uses to persist cassettes. By default that isn’t a bad format: it is readable and Ruby developers are generally familiar with it. But if you would want to change the serialization, you can set it in the configuration. That could come in handy, for example if you’re exporting the cassettes to a separate system that does not support YAML.

1
2
3
4
5
VCR.configure do |config|
  config.default_cassette_options = {
    serialize_with: :json
  }
end

The available serializers are: :yaml, :syck or :psych for YAML format, :json for JSON format (powered by MultiJson) or :compressed, which adds compression to the :yaml serializer. You can even create and use your own serializer. It should respond to file_extension, serialize and deserialize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module VcrXmlSerializer
  def file_extension
    'xml'
  end
  
  def serialize(hash)
    Qyoku.xml(hash, key_converter: :none)
  end
  
  def serialize(xml)
    Nori.new(parser: :nokogiri).parse(xml)
  end
end

VCR.cassette_serializers[:xml] = VcrXmlSerializer

VCR.configure do |config|
  config.default_cassette_options = {
    serialize_with: :xml
  }
end

2. You can change the persistence

By default VCR generates files, which works fine for most use cases. You can implement your own storage though, for example to write all interactions to a database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module VcrDatabasePersister
  def []=(file_name, content)
    db.execute("INSERT INTO http_interactions (name, content) VALUES (?, ?)", [name, content])
  end
  
  def [](file_name)
    db
      .get_first_row("SELECT content FROM http_interactions WHERE name = ?", [name])
      .fetch('content')
  end
  
  private
  
  def db
    SQLite3::Database.new("test.db")
  end
end

VCR.cassette_persisters[:database] = VcrDatabasePersister

VCR.configure do |config|
  config.default_cassette_options = {
    persist_with: :database
  }
end

3. You can hook into events

VCR supports a number of hooks that allows you to change data before it is used or persisted by VCR:

  • before_record: change the HTTP interaction before it is recorded
  • before_playback: change a cassette before it is registered for use
  • before_http_request, after_http_request and around_http_request to perform an action before and/or after a HTTP request is performed (either stubbed or real requests).

For example, you can wrap a timeout around each request:

1
2
3
4
5
VCR.configure do |config|
  config.around_http_request do |request|
    Timeout::timeout(500, &request)
  end
end

See: https://relishapp.com/vcr/vcr/v/5-0-0/docs/hooks

4. You can use placeholders

You can use the config option define_cassette_placeholder, aliased as filter_sensitive_data to replace data before it is written to a file. You can, for example, use it to filter passwords:

1
2
3
VCR.configure do |config|
  config.filter_sensitive_data('<MASKED>') { ENV['API_PASSWORD'] }
end

See: https://relishapp.com/vcr/vcr/v/5-0-0/docs/configuration/filter-sensitive-data

5. You can use cassette data inside your test

One use case is when a HTTP request is time sensitive (eg. when signing a request). When using use_cassette the cassette object is passed to the block, allowing to grab the recording time:

1
2
3
4
5
VCR.use_cassette('example') do |cassette|
  request = build_api_request
  timestamp = cassette.originally_recorded_at || Time.now
  request.sign_with('some secret key', timestamp)
end

See: https://relishapp.com/vcr/vcr/v/5-0-0/docs/cassettes/freezing-time

Besides the recording time, the cassette exposes more metadata like the name, configuration and the file path. For more on the public API of Cassette see: https://rubydoc.info/gems/vcr/VCR/Cassette.

6. You can use ERB in cassettes

Yes, you can use ERB in a cassette. There are probably not many cases where you should do it, but here is an example just to show what is possible. We use ERB to inject an API key in a cassette:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
http_interactions:
- request:
    method: get
    uri: http://example.com/time?api_key=<%= api_key %>
    body:
      encoding: UTF-8
      string: ''
    headers: {}
  response:
    status:
      code: 200
      message: OK
    headers:
      Content-Type:
      - application/json;charset=UTF-8
      Content-Length:
      - '47'
    body:
      encoding: UTF-8
      string: "{\"time\":\"2019-08-08T20:08:25.604188+02:00\"}"
    http_version: '1.1'
  recorded_at: Tue, 01 Nov 2011 04:58:44 GMT
recorded_with: VCR 2.0.0
1
2
3
4
VCR.use_cassette('time', erb: { api_key: ENV['TIME_API_KEY'] }) do
  response = Net::HTTP.get_response('example.com', "/time?api_key=#{ENV['TIME_API_KEY']}")
  puts "Response: #{response.body}"
end

See: https://relishapp.com/vcr/vcr/v/5-0-0/docs/cassettes/dynamic-erb-cassettes

That’s all, folks!

Hopefully these tips were useful to make testing your application easier. Do you need more help? At Kabisa we know a lot about writing good tests. Leave us a message if you would like to get in touch.

At Kabisa, privacy is of the greatest importance. We think it is important that the data our visitors leave behind is handled with care. For example, you will not find tracking cookies from third parties such as Facebook, Hotjar or Hubspot on our website. Only cookies from Google and Vimeo are used in order to improve the user experience of our visitors. These cookies also ensure that relevant advertisements are displayed. Read more about the use of cookies in our privacy statement.