#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)
外部Webサービスと通信するRailsアプリケーションを持っていたら、テストを楽にするVCRを使うことをお勧めします。Myron MarstonによるこのRuby gemで、テストの時に外部HTTPリクエストを「カセット」に記録することができます。テストが1度実行されたら、VCRは前に記録したリクエストとレスポンスを使用して、本物のリクエストとレスポンスをモック(mock)します。これを使うことによって、外部リクエストを行うことで余分な時間がかかってしまうことなしに実際のAPIをテストできるという利点があります。
VCRを使用してSOAPリクエストを記録する
今回のエピソードではVCRを使用して、前回作成したアプリケーションにテスト駆動開発によって機能を追加します。このアプリケーションはSOAP APIと通信してZipコードに対応する情報を取得します。APIとやりとりする部分のコードはまだ書いていないため、Zipコードを入力して「Lookup」をクリックしてもデータは何も表示されません。VCRとテスト駆動開発を用いてこのコードを追加していきます。
エピソード275で紹介したのと同じ方法で設定したテスト環境と、空のrequest specがあります。新しい機能をテストするために、specにコードを書きます。
require "spec_helper" describe "ZipCodeLookup" do end
作成するspecは、Zipコードを入力したときに正しい都市名が表示されるかどうかをチェックします。ハイレベルなrequest specの優れている点は、ブラウザを操作する手順を再現できるということなので、ZipコードのページにアクセスしてテキストフィールドにZipコードを入力して「Lookup」ボタンをクリックするというコードを書くことができます。
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
その機能はまだ作成していないため、テストを実行すると当然のこととして失敗します。これを成功させるために、前回書いたコードをペーストします。
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
テストスイートからの出力を見ると、テストは成功して外部データを取得するためのSOAPリクエストが作成されています。このケースではテストの実行に3秒かかっています。外部データと通信するテストをさらに追加すると、近々許容できないほど遅いテストスイートになってしまうでしょう。
VCRでテストをスピードアップする
VCRを使って、このテストの実行速度を速くします。そのために、アプリケーションにvcr
gem とHTTP mockingを扱うためのもう一つのgemを追加します。VCRはいくつかのHTTP mockingライブラリをサポートしていますが、もっとも人気があるのはFakeWebとWebMockです。 FakeWebの方が少し速い一方、WebMockはより多くの種類のHTTPライブラリをサポートしています。エピソード276でFakeWebを使用したので、ここでも再度それを使います。2つのgemを:test
グループに追加して、bundle
コマンドを実行してインストールします。
group :test do gem 'capybara' gem 'guard-rspec' gem 'vcr' gem 'fakeweb' end
VCRを使用できるようにするために設定をおこないます。VCRに対してカセットをどこに置くかと、どのライブラリをスタブ化するかを指定します。これを/spec/support
ディレクトリに新たに作成するvcr.rb
ファイルでおこないます。
VCR.config do |c| c.cassette_library_dir = Rails.root.join("spec", "vcr") c.stub_with :fakeweb end
もしVCRのバージョン2(現在はベータ)を使用している場合は、コマンドはconfig
ではなくconfigure
を使うことに注意してください。指定できる設定オプションについてさらに情報が必要であれば、VCRのRelishドキュメンテーションを参照してください。ここには役に立つ情報がたくさんあり、設定についてもまるごと1セクションが割かれています。
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
デフォルトではVCRは外部HTTPリクエストがVCRレコーダ以外で行われた場合に例外を投げるように設定されているので、その機能を利用するようにspecを修正します。specでVCRを有効化するためにVCR.use_cassette
を呼び出してカセットに名前をつけ、specの残りのコードをブロックの中に入れます。これでブロック内で出される外部HTTPリクエストはカセットに記録されます。(カセットの名前のスラッシュ(/)に注目してください。これはカセットがサブディレクトリに保存されることを意味します。)
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
次回specを実行するときには、外部HTTPリクエストがおこなわれたらカセットに保存されます。呼び出したWebサービスの実行速度が少し遅くなり、これによってspecが終了するまでに時間がかかるかも知れません。しかし同じspecを2度目に実行すると、VCRがリクエストを再生しカセットからレスポンスを取得するので、ずっと高速に実行されます。(今おこなったspecでは、15.49秒対1.09秒という結果になりました。)
カセットは/spec/vcr
ディレクトリに保存されます。カセットの名前をzip_code/90210
としたので、データはzip_code
ディレクトリの下の90210.yml
ファイルに保存されます。このファイルにはVCRが記録したものが、WSDLファイルから始まってリクエストとレスポンスと、すべて含まれます。
カセットの管理
今のところVCRは便利に使えていますが、使い続けるにしたがってすべてのカセットを管理するのが難しくなってきます。カセットを自動的に管理してくれるしくみがあれば便利なのですが、幸運なことに実はそれがあります。Relish documentationのRSpecページでuse_vcr_cassette
マクロについて言及されていてこれが便利なので、違うアプローチをとることにしてRSpecタグを代わりに使います。実現したいのは、VCRを使う必要があるspecに:vcr
タグを追加したら自動的にVCRを使うように設定され、specの名前に基づいてカセットが作成されるという状態です。具体的には以下のようなものです。
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
前に作成したvcr.rb
ファイルにRSpecの設定を追加することで、これをおこなうことが可能です。
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
上のコードの1行目によって、true
を指定しなくてもタグを追加できるようになります。つまり:vcr
タグを追加するだけで、:vcr => true
を書かなくてもよくなります。これにはRSpecの最新版が要求されるため、うまくいかなかい場合はアップグレードが必要かもしれません。
次にaround
ブロックがあります。これは:vcr
タグ付きのspecが見つかる度に実行されます。ブロックの最初の行は多少複雑に見えるかも知れませんが、単にspecの記述に基づいてカセットの名前を決定しているだけです。この名前を使って、新規のcassette
を作成し、specを呼び出します。spec毎に個別に:vcr
のタグを付けると、specの記述に基づいた名前のカセットでVCRを使用するようになります。
specを実行すると、前と変わらず成功し、zip_code_lookup
ディレクトリにshows_beverly_hills_given_90210.yml
という名前のカセットが作成されます。これらの名前はit
とdescribe
に渡される記述に基づいています。
カセットの設定
ときどき個々のカセットの振る舞いを個別に設定したいときがあるでしょう。例えばrecord
オプションを使用すると、いつVCRがリクエストをカセットに記録するかを細かく指定できます。デフォルトは:once
で、これはカセットは一度記録され、それ以降specを実行するときにはそれが再生されます。これを:new_episodes
に変更すると便利です。このオプションを使用すると、追加のリクエストがあるとそれが既存のカセットに追加されます。リクエストに秘密情報が含まれていて触りたくない、ただ再生するだけでいいという場合は:none
を使えます。最後に、まだアプリケーションの開発中でAPIを使って実験中の場合は:all
を指定するのがいいでしょう。この場合はカセットを再生することはなく、常に外部リクエストを発行します。このオプションはuse_cassette
を呼び出すときに指定します。例を以下に示します。
VCR.use_cassette('example', :record => :new_episodes) do response = Net::HTTP.get_response('localhost', '/', 7777) puts "Response: #{response.body}" end
specを介して:vcr
タグを指定するときに、以下のようにrecord
というもう一つのオプションを追加することでこれらのオプションを指定できたら便利でしょう。
describe "ZipCodeLookup" do it "shows Beverly Hills given 90210", :vcr, record: :all do #spec omitted end end
RSpecの設定を修正したときに書いたaround
ブロックを修正することでこれが可能です。このブロックでexample.metadata
を呼び出しますが、この中のハッシュには各specについての多くの情報があり、オプションを渡した場合にはそれも含まれています。slice
を使ってハッシュからこれらのオプションを抽出します。:record
オプションと:match_requests_on
オプションを取得します。しかしここで一つ問題があります。metadata
は単純なハッシュではなく、:example_group
というキーが残っているようです。そこでexcept
メソッドを使って、このキーを除外します。これで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
:all
を指定したので、外部リクエストはspecを実行するごとに発行され、それによって実行にかかる時間に遅れが発生します。
秘密情報の保護
APIに対して作業を行なっているときに、秘密キーを記録に含めたくないという場合もあるでしょう。これにフィルターをかけて除外することが重要です。今回のリクエストには該当するものはないですが、例を示すためにリクエストのuri
フィールドを秘密にしなくてはいけないと仮定します。
--- - !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.
VCR.config
ブロックの中のfilter_sensitive_data
というオプションを使って秘密情報にフィルタをかけます。このオプションは2つの引数をとります。一つ目はカセットに秘密情報のプレースホルダとして書き込まれる文字列、二つ目は置き換えたいテキストを返すブロックです。
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
specが次に実行されると、秘密情報は置き換えられました。
--- - !ruby/struct:VCR::HTTPInteraction request: !ruby/struct:VCR::Request method: :get uri: <WSDL> body: headers: # Rest of file omitted.
外部サイトへのリダイレクトを処理する
場合によってはユーザを外部のwebサイトにリダイレクトしてユニークなトークンとともに元のサイトに戻させなくてはいけないときもあります。これは、Twitterなどの第三者を介して認証をしなくてはいけない場合や、PayPalで支払いをおこなう場合などに発生します。このサイトではそのような状況は発生しませんが、Railscastsサイトで検索を行うspecを書いてこれを模擬できます。
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
Capybaraが外部サイトへのアクセスのしかたがわからないため、このテストは失敗します。Capybaraはその下でRack::Test
を利用していますが、これはRackアプリケーションをテストするためのものでHTTPの処理は全くわかりません。これへの対応として、Jeroen van DijkによるCapybara-mechanizeを使用します。このgemはその下でMechanizeを利用して外部URLにアクセスします。利用している他のgemと同じ方法でインストールできるので、:test
グループに追加しbundle
コマンドを実行します。
group :test do gem 'capybara' gem 'guard-rspec' gem 'vcr' gem 'fakeweb' gem 'capybara-mechanize' end
インストールされたら、spec_helper
ファイルでそれをrequireします。
# 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.
最後にspecを修正して、ドライバーとしてmechanize
を使用するようにします。
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
これらがすべて設定されるとspecは再度成功します。他のspecと同じように、これも最初の一回は外部リクエストを発行し、その結果を対応した名前のカセットに保存します。
出力を整理する
VCRを使用してテストを実行すると多くの情報が表示されるためにテストの出力がうるさく感じるかも知れません。前回のエピソードで紹介したSavonライブラリを使用している場合、spec_helperファイルに数行のコードを追加することで、この出力を表示させないようにもできます。
HTTPI.log = false Savon.log = false
specを実行すると、出力はずっときれいになりました。