If your Ruby application uses any kind of external API then you probably have faced the problem of slow tests & API rate limits.
What is the solution?
You could manually stub the HTTP methods from your client library, and return some pre-determined responses.
But that’s a lot of work & ugly code!
A better solution is to use a powerful combination of gems like Webmock + VCR.
WebMock intercepts HTTP requests from common HTTP libraries, like:
- net/http
- Faraday
- RestClient
- …a lot more!
This alone is useful, but you still have to provide the response data.
Here’s is where VCR comes in…
VCR works in combination with WebMock to record HTTP responses made by your code.
These recordings are called “cassettes”.
When you run your tests:
VCR will load the cassette files & return the recorded responses. You’ll get faster responses because you don’t have to ask the real API.
Let’s see some code examples!
VCR Code Example
For this example we are going to be using RSpec because it integrates better with VCR as you’ll see in the next section.
Here’s the code we want to test:
require "faraday" require "json" class Github def self.user(name) url = "https://api.github.com/users/#{name}" data = Faraday.get(url).body JSON.parse(data, symbolize_names: true) end end
It makes a request to the Github API to get information about a specific user. Pretty simple but it’ll help us learn about how VCR works.
A test for this code may look like this:
require "rspec/autorun" require_relative "github_api_example" describe Github do let(:user_response) { Github.user("ruby") } it "can fetch & parse user data" do expect(user_response).to be_kind_of(Hash) expect(user_response).to have_key(:id) expect(user_response).to have_key(:type) end end
This will hit the real API & pass, but it takes about half a second to finish.
Half a second for ONE test!
May not seem like much, but imagine that you have a hundred tests, that’s 50 seconds to run all of them.
Time to fix that…
Let’s introduce VCR by adding this bit of code:
require "vcr" VCR.configure do |c| c.cassette_library_dir = "spec/vcr" c.hook_into :webmock end
You can add this code in a
test_helper
file, so it’s available to all your tests
With this configure
block you’re telling VCR where to save the cassette files, and to enable the WebMock integration.
If you’re using Faraday, or Excon, then VCR can hook into those directly.
Just replace :webmock
with :faraday
, or :excon
.
Next:
You have to tell VCR the name of the cassettes & what code should run under these.
Here’s how:
let(:user_response) do VCR.use_cassette("github/user") { Github.user("ruby") } end
When you run your tests VCR will create a file under cassette_library_dir
, in this case the file name will be spec/vcr/github/user.yaml
, if you’re following along you may want to have a look at it.
If I run the tests now it will be a lot faster.
In fact…
It only takes 0.01 seconds to finish!
“An HTTP request has been made that VCR does not know how to handle”
If you run into this error message it means one of two things:
1.You’re trying to make an HTTP call with VCR enabled, but outside a VCR.use_cassette
block.
Solution: By default VCR + WebMock block all HTTP requests. You can change this with configuration options, add the missing VCR block, or use RSpec metadata (next section).
2.You’re trying to make a different request & use a cassette that doesn’t match this URL. For example, if you request in your tests /users/ruby
, the cassette gets created specifically for this URL, if you change the test to /users/apple
then you’ll see this error because the cassette is for a different URL.
Solution: Use different cassettes for different URLs, enable the new_episodes
recording mode (vcr: { record: :new_episodes }
) or delete the old cassette after you update the request URL.
How to Use RSpec Metadata
The VCR.use_cassette
method is a good way to use this gem.
But…
You can also let VCR create cassettes automatically for you.
How?
Add this inside the VCR.configure
block:
c.configure_rspec_metadata!
Now you can enable VCR for a particular test (it
block), or for a test group (describe
).
Like this:
describe Github, :vcr do # ... end
This will create cassette files named after the test description:
spec/vcr/ └── Github ├── can_parse_user_data.yml └── can_test_vcr.yml
Notice that this will create one cassette per test…
Even if two tests make the same request & use the same data. VCR will create separate cassettes for them.
Helpful VCR Options & Tips
If you are having issues with your cassettes, or if you want a fresh version of the data, then you can delete the cassette files.
VCR will record new API responses & that may solve your problem.
But what if that doesn’t?
You can enable debug mode in VCR.configure
:
VCR.configure do |c| # ... c.debug_logger = $stderr end
This may produce a lot of output, so limit your tests to only those you’re interested in.
Next:
Let’s say there are API keys, or other sensitive data as part of the API response…
You may want to filter that data from the recording.
Here’s how:
VCR.configure do |c| # ... c.define_cassette_placeholder("<API_KEY>", ENV["API_KEY"]) end
Summary
You have learned how to use the WebMock & VCR gems so you can write tests for your Ruby apps without having to wait for external API responses!
If you like this don’t forget to share this article so more people can enjoy it.
Thanks for reading 🙂
So if later someone changes field name in response to something else.
Your tests will still think that everything is good but in real life due to this change your code that doesn’t know how to handle that field will be broken? What the best practices of maintaining reliable tests with that approach?
Yes, if things change on the API side of things then you’ll not know about it.
It shouldn’t be an issue since most APIs use versioning. That means that v1 (or v2, etc.) of an API is set in stone, so you can rely on it.