Created
February 1, 2012 10:02
-
-
Save azuby/1716309 to your computer and use it in GitHub Desktop.
Show Notes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
Our zip code lookup application. | |
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. | |
/spec/request/zip_code_lookup_spec.rb | |
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. | |
/spec/request/zip_code_lookup_spec.rb | |
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. | |
/app/models/zip_code.rb | |
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. | |
/Gemfile | |
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. | |
/spec/support/vcr.rb | |
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.”. | |
terminal | |
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.) | |
/spec/requests/zip_code_lookup_spec.rb | |
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. | |
/spec/requests/zip_code_lookup_spec.rb | |
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. | |
/spec/support/vcr.rb | |
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: | |
ruby | |
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: | |
/spec/requests/zip_code_lookup_spec.rb | |
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. | |
/spec/support/vcr.rb | |
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. | |
/spec/vcr/zip_code_lookup/shows_beverley_hills_given_90210.yml | |
--- | |
- !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. | |
/spec/support/vcr.rb | |
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. | |
/spec/vcr/zip_code_lookup/shows_beverley_hills_given_90210.yml | |
--- | |
- !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. | |
/spec/requests/zip_code_lookup_spec.rb | |
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. | |
/Gemfile | |
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. | |
/spec/spec_helper.rb | |
# 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. | |
/spec/requests/zip_code_lookup_spec.rb | |
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. | |
/spec/spec_helper.rb | |
HTTPI.log = false | |
Savon.log = false | |
Now when we run our specs the output is much cleaner. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
group :test do | |
# ... | |
gem 'vcr' | |
gem 'fakeweb' | |
gem 'capybara-mechanize' | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'capybara/mechanize' | |
HTTPI.log = false | |
Savon.log = false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Filename: spec/support/vcr.rb | |
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 | |
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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Filename: spec/requests/zip_code_lookup_spec.rb | |
describe "ZipCodeLookup" do | |
it "show 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 | |
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 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment