Using the VCR gem in Ruby without making a mess
Here are some lessons I have learned (the hard way) about using VCR in Rails. They can be summed up like this:
- Recording cassettes should be easy
- Reusing cassettes should be hard
- How you use VCR will affect how you design your application – so use it wisely
I’ll explain what I mean in a bit more detail.
Recording should be easy
Much of the benefit you get from VCR over explicitly specified stubs (e.g. using WebMock) comes from the ease of recording. For example, you add some code that hits a new endpoint: fine, just record a new cassette. Or the interface of an API changes a bit: fine, just delete the old cassette and record a new one. If you never need to do either of those things, it’s worth thinking about whether you even need VCR, but that’s another topic that I won’t go into here.
So recording cassettes should be as frictionless as possible. If your setup requires any more preparation than setting up any needed authentication credentials (like changing URLs, or manually editing cassettes), chances are there’s something you could improve. For example:
If your cassettes contain secrets (e.g. authentication keys), use
filter_sensitive_data to remove them. If the secret is available at runtime while you’re recording a cassette (e.g. if you set a password as an environment variable), you can pass it into
filter_sensitive_data, but even if not, you can even filter using data from the actual HTTP request or response. For example, you can filter out whatever is in the
Authorization header, without specifying what it might be (very handy, for example, for filtering out base64-encoded basic authentication credentials).
Keeping URLs unchanged
You might be tempted to set the URL for any external services to something local (or made up) in the test environment. This feels like a nice and safe thing to do, to make sure tests never hit the real services, but it’s both unnecessary and unhelpful with VCR. VCR is already stopping the tests from hitting the real service, and more importantly, having to change URLs to record new cassettes adds friction. So in the test environment, point to the real service (or whatever the cassettes are actually recorded against).
Reuse should be hard
When recording cassettes is annoying, it becomes tempting to optimise for cassette reuse instead, so you can avoid the recording process as much as possible. This is an antipattern, in my experience.
One way this might manifest is in how you choose to name cassettes. When you use VCR with RSpec, the default behaviour if you don’t specify a cassette name is that the filename is inferred from the name of the spec, so that you will have a separate cassette file for each spec that uses VCR. If recording cassettes is hard, you might be tempted to start naming your cassettes in a way that facilitates reuse – like naming them after the resource they represent, for example.
Suppose your code involves getting information about cats from a cat API (this is a fictional example – I just like cats more than I like businessy things like orders or products). Now you add a new test that needs to hit the cat API. You could record a new cassette, but that would be annoying, so you find another test that’s already hitting the cat API, and you reuse its cassette. Because the cassette is no longer test-specific, you name it something like
cats_1234. You sit back and enjoy the sweet VCR life.
But this life is not as sweet as it seems. For example, what if one of the several tests that use the
cats cassette needs to hit another endpoint? Now the cassette is no longer just about
cats – it’s about
whatever_these_various_tests_need. And if one of the tests changes so that the cassette needs to be rerecorded, you need to make sure you rerecord it in a way that doesn’t affect those other tests.
Better, I would say, to stick with one cassette per test. Yes, there will be duplication – but, as the example above shows, the alternative to duplication is coupling, which I reckon is much worse. Besides, each cassette is now a nice, clean record of exactly what external resources a particular spec depends on, which makes it more meaningful, and less of a scary and mysterious black box. (You might still want to name it explicitly, instead of leaving it up to the RSpec integration to figure out a name – otherwise you might suddenly have to rename a file when you change a test description.)
Besides creating coupling between tests, cassette reuse might also be a way to avoid addressing deeper smells. If recording cassettes is starting to feel like a nuisance, chances are it’s not just because it’s not easy enough (see above) – you might be relying on it too much.
I’m going to make a bold claim here (and hope you agree): in a well-designed application, calls to external services should be encapsulated in modules whose only job is to talk to the external service, rather than spread out across other code with other jobs.
So when we use the cat API above, we shouldn’t litter (pun intended) our controllers or other classes with actual HTTP calls. We should probably create a
CatAPI class or something that is all about calling the cat API and nothing else. The specs for
CatAPI will, of course, need to use VCR, but for any other class that uses
CatAPI (except possibly some sort of end-to-end test), the specs can just stub the
If this is the case, having to record a cassette should never really come as a surprise. If you’re working on a class whose very purpose is making HTTP requests, of course you need to care about stubbing HTTP requests. If you’re writing a thorough end-to-end test that includes external services – ditto. But those are the exceptions; everywhere else, VCR shouldn’t even be necessary, because any interaction with external services should be neatly wrapped in a class that is itself easy to stub, mock or fake as required.
Bad design should be hard, good design should be easy
In the project that inspired all this, we had made rerecording hard, but reuse easy. This encouraged us to not think too carefully about where the seams were in the system, and how we were dealing with external dependencies – whenever a spec turned out to be making an HTTP request, instead of questioning whether it ought to be making one at all, we’d just throw in one of our reusable cassettes.
It wasn’t that we didn’t know all that stuff about encapsulation and so on – this was a pretty experienced team that spent a lot of time talking and thinking about design. But when there’s time pressure and stuff that needs to get done, there will always be a tendency towards the path of least resistance. But by thinking carefully about how we set up our tools, and what patterns we establish, we can affect what that path looks like – so even if we’re in a hurry and good design is not a priority, the patterns are more likely to nudge us towards good decisions anyway.