VCRを使って開発中にあほみたいにリクエストを飛ばさないようにする
RubyでWebサイトからスクレイピングしまくってデータを取得しまるくるようなプログラムを書いていると開発のテスト中にリクエストをあほみたいに飛ばしてしまうときがあります。
そうするとそのサーバーに迷惑がかかってしまいますし、あんまりいい気持ちのするものでもありません。そこで今回はVCRというgemを使ってこの問題を解決してみたいと思います。
VCRとは
VCRとはテスト中に過去に投げたリクエストをカセット(cassete)に記録しておいて、その後再度同じリクエストが投げれられた場合はサーバーではなく、記録しておいたカセットからリクエストの結果を取り出すというものです。
これによって、上記のようにサーバーに迷惑がかかることもなくなりますし、テストのスピードも格段に早くなります。
簡単な使い方
とりあえずどんな感じになるのか、上記githubに載っているsampleをrspecバージョンで書き換えて簡単なサンプルを試してみたいと思います。
まず必要なgemがインストールされていない場合はしておきます。
gem install vcr rspec webmock
そして
spec --init
を実行 するとspecファイルが生成されます
create .rspec create spec/spec_helper.rb
続いて、sample_spec.rbを生成します。
touch sample_spec.rb
まずはspec_helperの末尾にVCRの設定を書き込んでいきます。
# spec_helper.rb VCR.configure do |config| config.cassette_library_dir = "vcr/vcr_cassettes" config.hook_into :webmock end
config.cassette_library_dirで記録したリクエストのymlファイルを保存しておくディレクトリを設定して、config.hook_intoでHTTPリクエストをどのようにhookするかを設定しています。
その他の設定できる項目については公式サイトに記載がありますので興味のある方は見てみてください。 Configuration - Vcr - VCR - Relish
続いてsample_spec.rbに実行するテストを書き込んでいきます
# sample_spec.rb require_relative './spec_helper.rb' require 'vcr' RSpec.describe 'sample' do it 'sample' do VCR.use_cassette("get_google") do 100.times do response = Net::HTTP.get_response(URI('https://www.google.com')) end end end end
googleに100回getアクセスするというプログラムです。google先生なら100回くらい大丈夫でしょう。
ポイントはVCR.use_cassette(カセット名) do endの部分です。こちらでどのカセットを使うのかを指定しています。
今はまだget_googleという名前のカセットはありませんが、1度このプログラムを実行することで、カセットが作成されます。
では1度目のテストを実行してみましょう。
rspec sample.rb
すると以下のような結果が出力されました。
Finished in 5.16 seconds (files took 0.66782 seconds to load) 1 example, 0 failures
100回getをするのに5.16秒かかりました。
ここでvcr/vcr_cassetes/ディレクトリを見てみるとget_google.ymlができているのがわかります。
中身を見てみると1回目のテストで実行されたリクエストが記録されているのがわかります。
ではもう一度テストを実行してみます
$ rspec sampler.rb $ Finished in 0.24276 seconds (files took 0.47402 seconds to load) 1 example, 0 failures
!!!早い!!超早い!!!これは実際にリクエストを投げたわけではなくカセットに記録しておいたリクエストの結果を読み込んでいるからです。
VCRすごいですね!問題が見事に解決されました。
こんなところで簡単な使い方はOKかと思います。
よく出るエラーの対策
例えば先ほどのsample_spec.rbをこんな風に書き換えたとします。
# sample_spec.rb require_relative './spec_helper.rb' require 'vcr' RSpec.describe 'sample' do it 'sample' do VCR.use_cassette("get_google") do 100.times do response = Net::HTTP.get_response(URI('https://www.yahoo.co.jp')) #google→yahooに書き換え end end end end
で同じテストを実行しようとするとこんなエラーが吐かれます
1) sample sample Failure/Error: response = Net::HTTP.get_response(URI('https://www.yahoo.co.jp')) VCR::Errors::UnhandledHTTPRequestError: ================================================================================ An HTTP request has been made that VCR does not know how to handle: GET https://www.yahoo.co.jp/ VCR is currently using the following cassette: - /Users/user_name/Desktop/GitHub/vcr_sample/spec/fixtures/vcr_cassettes/get_google.yml - :record => :once - :match_requests_on => [:method, :uri] Under the current configuration VCR can not find a suitable HTTP interaction to replay and is prevented from recording new requests. There are a few ways you can deal with this: * If you're surprised VCR is raising this error and want insight about how VCR attempted to handle the request, you can use the debug_logger configuration option to log more details [1]. * You can use the :new_episodes record mode to allow VCR to record this new request to the existing cassette [2]. * If you want VCR to ignore this request (and others like it), you can set an `ignore_request` callback [3]. * The current record mode (:once) does not allow new requests to be recorded to a previously recorded cassette. You can delete the cassette file and re-run your tests to allow the cassette to be recorded with this request [4]. * The cassette contains 100 HTTP interactions that have not been played back. If your request is non-deterministic, you may need to change your :match_requests_on cassette option to be more lenient or use a custom request matcher to allow it to match [5]. [1] https://www.relishapp.com/vcr/vcr/v/3-0-0/docs/configuration/debug-logging [2] https://www.relishapp.com/vcr/vcr/v/3-0-0/docs/record-modes/new-episodes [3] https://www.relishapp.com/vcr/vcr/v/3-0-0/docs/configuration/ignore-request [4] https://www.relishapp.com/vcr/vcr/v/3-0-0/docs/record-modes/once [5] https://www.relishapp.com/vcr/vcr/v/3-0-0/docs/request-matching ================================================================================
ooops
An HTTP request has been made that VCR does not know how to handle: GET https://www.yahoo.co.jp/
とあるようにそんなリクエストの返し方知らねーよバカって書いてます。
当たり前といえば当たり前で1回目に記録したgoogleに対するアクセス記録を使ってyahooにアクセスしようとしてるわけですから知らなくて当然です。
このことから、デフォルトでは保存されているアクセスはカセットから返して、保存されていないものは新たに記録するといったよしなな感じになっていないことが分かります。
でも結構親切に対策方法を書いてくれているので安心です。1つずつ見ていきましょう
1つ目
If you're surprised VCR is raising this error and want insight about how VCR attempted to handle the request, you can use the debug_logger configuration option to log more details [1].
「エラーの原因とかもうちょい詳しく知りたかったらログとか出してみたらいいと思うよ」って言ってます。
ログを取ってみましょう。spec_helper.rbにログファイルを生成する設定を記載します。
# spec_helper.rb VCR.configure do |config| config.cassette_library_dir = "fixtures/vcr_cassettes" config.hook_into :webmock # or :fakeweb config.debug_logger = File.open("log","w") end
これでテストを実行すると、logファイルが生成されるので中身を見てみます。
上略... [Cassette: 'get_google'] Checking if [get https://www.yahoo.co.jp/] matches [get https://www.google.com/] using [:method, :uri] [Cassette: 'get_google'] method (matched): current request [get https://www.yahoo.co.jp/] vs [get https://www.google.com/] [Cassette: 'get_google'] uri (did not match): current request [get https://www.yahoo.co.jp/] vs [get https://www.google.com/] [webmock] Identified request type (unhandled) for [get https://www.yahoo.co.jp/]
ひたすら100回分のgoogleに対するgetが読み込まれたのに(なのでほんとはdo loopの中にuse cassetteを書くべきでしたね。。) 最後にyahooに対するgetが呼ばれてそれは知らないとなってますね。 今回に関して言えば原因がわかっているのであまり有用な情報ではありませんが、原因がわからないときには有効な手法です。
続いて2つ目
You can use the :new_episodes record mode to allow VCR to record this new request to the existing cassette [2].
「:new_episodesっていうオプションつけたらこの新しいリクエストをexisting cassetteに記録できるよ!」って言ってます。
これやりたかったことっぽいですね。やってみましょう
# sample_spec.rb VCR.use_cassette("get_google", :record => :new_episodes) do #:record => :new_episodesオプションを追加 100.times do response = Net::HTTP.get_response(URI('https://www.yahoo.co.jp')) end end
これで実行すると先ほどのエラーは出ずに、get_google.ymlの末尾にyahooへのgetが記録されているのがファイルをみると確認できるかと思います。どんどん新しいものを同じカセットに記録していきたいならこの設定にしておけば良さそうです。
続いて
If you want VCR to ignore this request (and others like it), you can set an `ignore_request` callback
「VCRに無視してほしかったらignore_requrestして無視してちょーだい」って言ってます。
やってみましょう。
まずsample_spec.rbを書き換えます
# sample_spec.rb require_relative './spec_helper.rb' require 'vcr' RSpec.describe 'sample' do it 'sample' do VCR.use_cassette("get_google") do response = Net::HTTP.get_response(URI('https://github.com/')) #githubに書き換え end end end
これをこのまま実行すると、上記のunhandledのエラーが出てしまいます。
そこでgithubへのリクエストは無視するという設定をspec_helper.rbに追加します
# spec_helper.rb VCR.configure do |config| config.cassette_library_dir = "fixtures/vcr_cassettes" config.hook_into :webmock # or :fakeweb config.debug_logger = File.open("log","w") config.ignore_request do |request| #config.ignoreを追記 request.uri == 'https://github.com/' #uriがgithubの場合は無視する end end
これで再度テストを実行するとテストが通って、うまくgithubが無視されているのが分かります。
4つ目
The current record mode (:once) does not allow new requests to be recorded to a previously recorded cassette. You can delete the cassette file and re-run your tests to allow the cassette to be recorded with this request [4].
「今のrecord mode(once)だと新しいリクエストを記録できないっす。ファイル消してもう一回実行したらできます。」って言ってます。
ファイルを消してもう一回実行したらできるのは当たり前として、record modeが気になるところですね
こちらに詳しく書いていますが、ざっくりまとめると
:once
過去のものを再生できる
同名カセットファイルの記録は1度のみ
同名のカセットファイルがある場合はエラーを吐く(←今回出ているエラーはこれですね)
:new_episodes
過去のものを再生できる
同名のカセットファイルがあれば追記する
:none
過去のものを再生できる
新たな記録は行わない
:all
- 過去に記録されててもとにかく全部記録する
のような感じになっています。
1つ注意点としてallにすると過去のものを記録して再生できるというvcrの利点が生かされずに全てカセットに記録するということになってしまいます。
なので先ほどやったnew_episodesというオプションはこのrecord_modeを設定していたのです。
最後
The cassette contains 101 HTTP interactions that have not been played back. If your request is non-deterministic, you may need to change your :match_requests_on cassette option to be more lenient or use a custom request matcher to allow it to match [5].
「カセットに101(Switching Protocol)が含まれてると再生できないよー。match_requests_onの条件もっとゆるくするかカスタムのrequest matcher定義してね。」って言ってます
要は記録したリクエストに101が含まれていると、同じリクエストを再生しても違うリクエストと判断されるからうまくリクエストmatcherを設定してくれってことだと思います。
軽く調べてみましたが、match_requests_onの条件をゆるくする方法についてはちょっと良く分かりませんでした。
また、request matchを自前で定義する方法については
describe "#fetch_info", vcr: {match_requests_on: [:method, VCR.request_matchers.uri_without_param(:timestamp)]} do # この場合timestampはmacherの条件がから除外される end
こんな感じで書くといいみたいです。
最後だけちょっと曖昧になってしまいましたが、こんなところで終わりにしたいと思います。
参考:
追記 VCRで記録したcasetteが間違ったレスポンスを返していることがありハマりました。もしリクエストのログとブラウザで実際アクセスしたリクエスト内容が合ってるにも関わらずレスポンスがおかしい時はcassetteの記録がおかしいことを疑ってみてもいいかもしれません。