#291 Testing with VCR pro
- Download:
- source codeProject Files in Zip (89 KB)
- mp4Full Size H.264 Video (35.7 MB)
- m4vSmaller H.264 Video (17.4 MB)
- webmFull Size VP8 Video (17.6 MB)
- ogvFull Size Theora Video (45.6 MB)
If you have a Rails application that communicates with an external web service you should be using VCR to help with testing. This Ruby gem, written by Myron Marston, can be used in your tests to record external HTTP requests to ‘cassettes’. Once a test has been run once VCR will use the request and response it recorded earlier to mock the real request and response. This gives us the benefit of testing the actual API without the penalty of the time it takes to make an external request and also means that we can run our tests offline once the cassette has been recorded.
Using VCR to Record a SOAP Request
In this episode we’ll use VCR to add features via test-driven development to the application we built last time. This application communicates with a SOAP API to fetch information about a Zip code. We haven’t written the code that talks to the API yet, so when we enter a Zip code and click ‘Lookup’ we don’t see any data. We’ll use VCR and test-driven development to add this code.
We already have our test environment set up in the same way we showed in episode 275 and an empty request spec. We’ll write the code in that spec to test-drive the new functionality.
require "spec_helper" describe "ZipCodeLookup" do end
The spec we’ll write will check that the correct city name is shown when we enter a Zip code. The beauty of high-level request specs is that we can duplicate the steps we took in the browser, so we can write code to visit the zip code page, fill in the text field and click the “Lookup” button.
require "spec_helper" describe "ZipCodeLookup" do it "shows Beverly Hills given 90210" do visit root_path fill_in "zip_code", with: "90210" click_on "Lookup" page.should have_content("Beverly Hills") end end
Unsurprisingly the test fails when we run it as we haven’t written that functionality yet. To get it to pass we’ll paste in the code we wrote last time.
class ZipCode attr_reader :state, :city, :area_code, :time_zone def initialize(zip) client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL") response = client.request :web, :get_info_by_zip, body: { "USZip" => zip } data = response.to_hash[:get_info_by_zip_response][:get_info_by_zip_result][:new_data_set][:table] @state = data[:state] @city = data[:city] @area_code = data[:area_code] @time_zone = data[:time_zone] end end
If we look at the output from our test suite now we’ll see that the test has passed and that it’s made a SOAP request to get the external data. In this case it’s taken nearly three seconds for the test to run. If we add more tests that communicate with external data then we’ll soon have an unacceptably slow test suite.
Speeding up Tests With VCR
We’ll use VCR to make this test run faster. To use it we’ll need to add the vcr
gem to our application along with another gem to handle HTTP mocking. VCR supports a number of HTTP mocking libraries the most popular of which are FakeWeb and WebMock. FakeWeb is a little faster but WebMock supports a wider range of HTTP libraries. We used FakeWeb in episode 276 so we’ll use it again here. All we need to do is add both gems to the :test
group and then run bundle
to install.
group :test do gem 'capybara' gem 'guard-rspec' gem 'vcr' gem 'fakeweb' end
Before we can use VCR we’ll need to configure it. We need to tell VCR where to put its cassettes and which library to stub with. We’ll do this in a new vcr.rb
file in the /spec/support
directory.
VCR.config do |c| c.cassette_library_dir = Rails.root.join("spec", "vcr") c.stub_with :fakeweb end
Note that if you’re using version 2.0 of VCR, currently in beta, this command is configure
rather than config
. For more information on the configuration options we can pass in take a look at the Relish documentation for VCR. There’s a lot of useful information here, including a whole section on configuration.
Now that we’ve set up VCR our test fails again with a error message telling us that “Real HTTP connections are disabled.”.
1) ZipCodeLookup shows Beverly Hills given 90210 Failure/Error: click_on "Lookup" FakeWeb::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: GET http://www.webservicex.net/uszip.asmx?WSDL. You can use VCR to automatically record this request and replay it later. For more details, visit the VCR documentation at: http://relishapp.com/myronmarston/vcr/v/1-11-3
By default VCR is configured so that it will throw an exception if any external HTTP requests are made outside of a VCR recorder so we’ll modify our spec to use it. We enable VCR in our spec by calling VCR.use_cassette
, giving the cassette a name and putting the rest of the spec’s code in a block. Any external HTTP requests made inside the block will now be recorded to the cassette. (Note the slash in the cassette’s name. This means that the cassette will be stored in a subdirectory.)
require "spec_helper" describe "ZipCodeLookup" do it "shows Beverly Hills given 90210" do VCR.use_cassette "zip_code/90210" do visit root_path fill_in "zip_code", with: "90210" click_on "Lookup" page.should have_content("Beverly Hills") end end end
The next time we run the spec the external HTTP request will be made and stored in the cassette. The web service we call can be a little slow to run and this will cause the spec can take a while to complete. When we run the same spec a second time, though, it runs far more quickly as VCR replays the request and fetches the response from the cassette. (For the run we’ve just done this was 15.49 seconds vs 1.09).
The cassettes are stored in the /spec/vcr
directory. As we called our cassette zip_code/90210
its data will be stored in a 90210.yml
file under a zip_code
subdirectory. This file contains everything that VCR recorded, starting with the WSDL file and followed by the request and the response.
Managing Cassettes
VCR is working well for us so far but the more we use it the more difficult it will become to manage all of the cassettes. It would be useful if there was an automated way to mange the cassettes and fortunately there is. The RSpec page of the Relish documentation mentions a use_vcr_cassette
macro and while this is useful we’re going to take a different approach and use RSpec tags instead. What we’d like to be able to do is add a :vcr
tag to the specs that need to use VCR so that they use it automatically and create a cassette based on the spec’s name, something like this.
require "spec_helper" describe "ZipCodeLookup" do it "shows Beverly Hills given 90210", :vcr do visit root_path fill_in "zip_code", with: "90210" click_on "Lookup" page.should have_content("Beverly Hills") end end
We can do this by adding some RSpec configuration to the vcr.rb
file we created earlier.
RSpec.configure do |c| c.treat_symbols_as_metadata_keys_with_true_values = true c.around(:each, :vcr) do |example| name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_") VCR.use_cassette(name) { example.call } end end
The first line in the code above allows us to add tags without needing to specify true
. This means that we can just add a :vcr
tag without needing to write :vcr => true
. This requires the latest version of RSpec so if it doesn’t work for you might need to upgrade.
Next we have an around
block. This is executed every time a spec with the :vcr
tag is found. The first line in the block looks rather complicated but all it does is determine a name for the cassette, based on the spec’s description. We use this name to create a new cassette
and then call the spec. Now, each time we tag a spec with :vcr
it will use VCR with a cassette with a name based on the spec’s description.
When we run our spec now it still passes and the cassette called shows_beverly_hills_given_90210.yml
is created in a zip_code_lookup
directory, these names being based on the descriptions passed to it
and describe
.
Configuring Cassettes
Sometimes we need to configure the behaviour of individual cassettes. For example there’s a record
option that allows us to specify exactly when VCR should record requests to a cassette. The default is :once
which means that a cassette will be recorded once and played back every time the spec is run afterwards. Alternatively, :new_episodes
is useful. If we use this option any additional requests that are found will be added to an existing cassette. If some requests are sensitive and we don’t ever want to hit them, only ever play them back, we can use :none
. Finally :all
works well while we’re still developing an application and experimenting with the API. This will never play a cassette back, but will always make the external request. We can specify this option when we call use_cassette
, like this:
VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end
It would be useful if we could pass one of these options in through our spec when we specify the :vcr
tag by adding another option called record
, like this:
describe "ZipCodeLookup" do it "shows Beverly Hills given 90210", :vcr, record: :all do #spec omitted end end
We can do this by modify the around
block we wrote when we modified RSpec’s configuration. In this block we call example.metadata
and this contains a hash of a lot of information about each spec, including any options we pass in. We can extract these options from the hash using slice
. We’ll get the :record
option and also the :match_requests_on
option. There is a problem here, however. The metadata
isn’t a simple hash and it seems to persist a key called :example_group
. We’ll use the except
method to exclude that key. We can then pass in the options to use_cassette
.
RSpec.configure do |c| c.treat_symbols_as_metadata_keys_with_true_values = true c.around(:each, :vcr) do |example| name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_") options = example.metadata.slice(:record, :match_requests_on).except(:example_group) VCR.use_cassette(name, options) { example.call } end end
As we’ve specified :all
the external request will now be made each time the spec runs, with the consequent delay in the time it takes to run.
Protecting Sensitive Data
Often when working with an API you’ll have a secret key that you don’t want to be included in the recordings and it’s important to filter these out. We don’t have one for our request, but for the sake of an example we’ll say that the uri
field for the request should be kept secret.
--- - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: http://www.webservicex.net:80/uszip.asmx?WSDL body: headers: # Rest of file omitted.
We filter sensitive by using an option called filter_sensitive_data
inside our VCR.config
block. This option takes two arguments: the first is a string that will be written to the cassette as a placeholder for the sensitive information while the second is a block that should return the text that we want to be replaced.
VCR.config do |c| c.cassette_library_dir = Rails.root.join("spec", "vcr") c.stub_with :fakeweb c.filter_sensitive_data('<WSDL>') { "http://www.webservicex.net:80/uszip.asmx?WSDL" } end
When the spec next runs our ‘sensitive’ data has been replaced.
--- - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: <WSDL> body: headers: # Rest of file omitted.
Handling Redirects to External Sites
Sometimes we need to redirect users to an external website and then have them return to our site with a unique token. This can happen when they need to authenticate through a third party such as Twitter, or when they make a payment with PayPal. We don’t have such a situation on this site, but we can simulate it by writing a spec that performs a search on the Railscasts site.
it "searches RailsCasts" do visit "http://railscasts.com" fill_in "search", with: "how I test" click_on "Search Episodes" page.should have_content('#275') end
This test will fail because Capybara doesn’t know how to visit external websites. It uses Rack::Test
underneath which is designed to test Rack applications and doesn’t know how to handle HTTP at all. We can work around this by using Capybara-mechanize by Jeroen van Dijk. This gem uses Mechanize underneath to visit external URLs. It’s installed in the same way as the other gems we’ve used by adding to the :test
group and running bundle
.
group :test do gem 'capybara' gem 'guard-rspec' gem 'vcr' gem 'fakeweb' gem 'capybara-mechanize' end
Once it’s installed we’ll need to require it in the spec_helper
file.
# This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'capybara/rspec' require 'capybara/mechanize' # rest of file omitted.
Finally we’ll need to modify the spec so that it uses mechanize
as its driver.
it "searches RailsCasts", :vcr do Capybara.current_driver = :mechanize visit "http://railscasts.com" fill_in "search", with: "how I test" click_on "Search Episodes" page.should have_content('#275') end
With all this in place our spec now passes again. As with our other spec this one will make an external request the first time it runs and store the result in a cassette based on its name.
Tidying up The Output
When we run tests that use VCR they show a lot of information that can make the test output noisy. We can stop this output from being displayed, though, by adding a couple of lines of code to the spec_helper file, as long we we’re using the Savon library as we did in the previous episode.
HTTPI.log = false Savon.log = false
Now when we run our specs the output is much cleaner.