Testing External Services with RSpec, VCR, and WebMock in Ruby on Rails
For quick setup, just run:
rails app:template LOCATION='https://railsbytes.com/script/X8Bsyo'
Introduction
When building Rails applications that integrate with external services, it's crucial to have reliable tests that don't depend on actual HTTP requests. This tutorial will show you how to use VCR and WebMock to record and replay HTTP interactions in your RSpec tests.
Setup
First, add these gems to your Gemfile
:
group :test do
gem 'rspec-rails'
gem 'vcr'
gem 'webmock'
end
Run bundle install
to install the gems.
Configuring VCR and WebMock
Create a new file at spec/support/vcr.rb
:
require 'vcr'
require 'webmock/rspec'
# Disable all real HTTP connections, except to localhost
WebMock.disable_net_connect!(allow_localhost: true)
VCR.configure do |c|
# Where VCR will store the recorded HTTP interactions
c.cassette_library_dir = Rails.root.join('spec', 'cassettes')
# Ignore localhost requests
c.ignore_localhost = true
# Tell VCR to use WebMock
c.hook_into :webmock
# Allow VCR to be configured through RSpec metadata
c.configure_rspec_metadata!
# Allow real HTTP connections when no cassette is in use
c.allow_http_connections_when_no_cassette = true
# Configure how requests are matched
c.default_cassette_options = {
match_requests_on: [:method, :uri, :body]
}
# Filter sensitive data before recording
c.before_record do |interaction|
interaction.request.headers['Authorization'] = '[FILTERED]'
end
# Ignore specific hosts
ignored_hosts = ['codeclimate.com']
c.ignore_hosts *ignored_hosts
end
# Configure RSpec to use VCR
RSpec.configure do |config|
config.around(:each) do |example|
options = example.metadata[:vcr] || {}
options[:allow_playback_repeats] = true
if options[:record] == :skip
VCR.turned_off(&example)
else
custom_name = example.metadata[:vcr]&.delete(:cassette_name)
generated_name = example.metadata[:full_description]
.split(/\s+/, 2)
.join('/')
.underscore
.tr('.', '/')
.gsub(/[^\w\/]+/, '_')
.gsub(/\/$/, '')
name = custom_name || generated_name
VCR.use_cassette(name, options, &example)
end
end
end
Using VCR in Your Tests
Here are some examples of how to use VCR in your specs:
Basic Usage
# spec/services/weather_service_spec.rb
RSpec.describe WeatherService do
describe '#get_forecast' do
it 'fetches weather data from the API' do
service = WeatherService.new
forecast = service.get_forecast('London')
expect(forecast).to include('temperature')
end
end
end
Custom Cassette Names
RSpec.describe WeatherService do
describe '#get_forecast' do
it 'fetches weather data', vcr: { cassette_name: 'weather_api/london_forecast' } do
# Your test code
end
end
end
It will make a first connection to the external service and record the response into a cassete
Skipping VCR
it 'makes real HTTP requests', vcr: { record: :skip } do
# VCR will be disabled for this test
end
Best Practices
- Sensitive Data: Always filter sensitive information like API keys and tokens:
VCR.configure do |c|
c.filter_sensitive_data('<API_KEY>') { ENV['API_KEY'] }
end
- Match Request Bodies: When testing POST requests, make sure to match on body content:
vcr: {
match_requests_on: [:method, :uri, :body],
record: :new_episodes
}
- Regular Cassette Updates: Periodically re-record your cassettes to ensure they match current API responses:
Troubleshooting
Common Issues
-
Unmatched Requests: If VCR can't find a matching request, check:
- Request method (GET, POST, etc.)
- URL (including query parameters)
- Request body
- Headers (if matching on headers)
-
Playback Failures: If recorded responses aren't playing back:
- Verify the cassette file exists
- Check if the request matching criteria are too strict
- Ensure sensitive data is being filtered consistently
Conclusion
VCR and WebMock provide a robust solution for testing external service integrations in Rails. By following these patterns and best practices, you can create reliable, maintainable tests that don't depend on external services being available.
Remember to:
- Filter sensitive data
- Organize cassettes logically
- Update cassettes periodically
- Match requests appropriately
- Handle errors gracefully