TIL about VCR
Tags: vcr
Published: 2020-02-20 12:33:39

---
title: TIL about VCR
published: true
date: 2019-11-04 00:00:00 UTC
tags: VCR
canonical_url: http://www.theguild.nl/til-about-vcr
---

A while ago I contributed a change to the well-known Ruby gem [VCR](https://github.com/vcr/vcr). You can read all about it in [this pull request](https://github.com/vcr/vcr/pull/765), 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.

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

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

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

```ruby
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](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:

```
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](https://relishapp.com/vcr/vcr/v/5-0-0/docs/configuration/filter-sensitive-data0)

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

```ruby
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](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](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:

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

```ruby
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](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](https://www.kabisa.nl/contact/) if you would like to get in touch.