TIL about VCR

MartijnPosted by Martijn Versluis on 4-11-2019

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:

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.

Bij Kabisa staat privacy hoog in het vaandel. Wij vinden het belangrijk dat er zorgvuldig wordt omgegaan met de data die onze bezoekers achterlaten. Zo zult u op onze website geen tracking-cookies vinden van third-parties zoals Facebook, Hotjar of Hubspot. Er worden alleen cookies geplaatst van Google en Vimeo. Deze worden gebruikt voor analyses, om zo de gebruikerservaring van onze websitebezoekers te kunnen verbeteren. Tevens zorgen deze cookies ervoor dat er relevante advertenties worden getoond. Lees meer over het gebruik van cookies in ons privacy statement.