Ruby
Rails
vcr

vcrでコマンドライン(環境変数)でrecordモードを切り替える、他

More than 1 year has passed since last update.

基本的な使い方は以下を参照

vcr (GitHub)
documentation (Relish)
RailsCasts #291 Testing with VCR

vcrでコマンドラインからrecordモードを切り替える

コマンドラインから以下のように、recordモードを上書きできる

$ RECORD=all rspec spec/foo_spec.rb

$ RECORD=new_episodes rspec spec/bar_spec.rb:15

spec_helperなどに以下を記述 (default_cassette_optionsの部分)

spec/spec_helper.rb
VCR.configure do |c|
  c.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
  c.hook_into :webmock # or :fakeweb

  c.default_cassette_options = { record: ENV.fetch('RECORD'){ :once }.to_sym }
end

これをしないと、記録状態を変更しようとするたびに describe "foo", record: :all do といったrecordオプションを付加しないといけないのでわずらわしい。(通常commit前に消す必要もあり面倒)

参考1 参考2 参考3

vcrのcassettesの生成した内容を見やすく変換する

vcrのcassettesのymlファイルを見てもレスポンスのbodyが読めない

Guardのプラグインを使って、ymlの隣にtxtを出力する (webmockは動作確認済み)
puts, f.writeの部分は好きなように変更すると良い

Guardfile
require 'guard/plugin'
require 'json'

module ::Guard
  class VcrReader < ::Guard::Plugin
    def run_all
      target_path = 'spec/fixtures/vcr_cassettes'
      puts "Converting all request/response data in #{target_path}."
      Dir.glob("#{target_path}/**/*\.yml").each do |path|
        generate_pretty_log_from_yml(path)
      end
    end

    def run_on_changes(paths)
      puts "Converting request/response data. #{paths.inspect}"
      paths.each do |path|
        next unless File.exists?(path)
        log_path = generate_pretty_log_from_yml(path)
        puts "Generated #{log_path}"
      end
    end

    private

      def generate_pretty_log_from_yml(path)
        record = YAML.load_file(path)
        log_path = path.gsub(/\.yml/, '.txt')
        File.open(log_path, 'w') do |f|
          record["http_interactions"].each do |interaction|
            request_method = interaction["request"]["method"]

            f.write('='*80+"\n")
            f.write('Request '+'-'*80+"\n")
            f.write(interaction["recorded_at"]+"\n")
            f.write("#{request_method.upcase} #{interaction["request"]["uri"]}"+"\n")
            f.write(arrange_body_string interaction["request"]["body"]["string"]) if request_method == 'post'
            f.write("\n")
            f.write('Response '+'-'*80+"\n")
            f.write(arrange_body_string interaction["response"]["body"]["string"])
            f.write("\n")
          end
        end
        log_path
      end

      def arrange_body_string(string)
        begin
          JSON.pretty_generate JSON.parse(string)
        rescue
          string
        end
      end
  end
end

guard :vcr_reader do
  watch(%r{^spec/fixtures/vcr_cassettes/.+\.yml$})
end

出力の変換結果例

================================================================================
Request --------------------------------------------------------------------------------
Tue, 08 Jul 2014 04:02:06 GMT
POST http://test.com/login.json
{
  "body": {
    "login": "test_user",
    "password": "secret"
  }
}
Response --------------------------------------------------------------------------------
{
  "body": {
    "loign": "test_user",
    "code": "ABC",
    "name": "テストユーザー",
    "lang": "ja"
  },
  "status": "success"
}
================================================================================
Request --------------------------------------------------------------------------------

...

録画した内容(cassettes内のリクエスト)の一致ルール match_requests_on

日時、日付など動的な特定のパラメータの値を無視するようにする

日時、日付など特定のパラメータの値をテストで使っていると、日付が変わったり、年度が変わったりするとcassetteが見つからないエラーで失敗するようになるテストができる。(例: date, starts_at, start_time, yearなど)

日付以外にも、毎回変わるものの例に新規作成で発行されたIDなどがある(createしてそのIDを元に以降のページをテストしている場合など)

それらのパラメータをcassetteのマッチング時に無視するような設定は以下のようにできる。

describe "SomeTest with Date", vcr: true, match_requests_on: [:method, VCR.request_matchers.uri_without_param(:date)] do

URI without param(s)

具体的なエラーの例

VCR::Errors::UnhandledHTTPRequestError:

========================================
  An HTTP request has been made that VCR does not know how to handle
    GET http://example.com/foo?date=20151119&...

パラメータ全体を無視する

パラメータ全体を無視することも可能
optionの:body, :queryなどを除く (match_requests_on: [:method, :path])

ただし、この場合は副作用が起こることがある。
同じpathに対して違うリクエストをするものが同一cassettesに入った時などに、すべて同じ(最初にマッチした)リクエストが返されてしまう。

match_requests_onの設定を調べる

各specの各exampleでどの設定になっているかを調べるには
(注. do |example| に変更)

  it "..." do |example|
    puts example.metadata[:match_requests_on]
    ...

デフォルトの設定を調べるにはVCR.configuration.default_cassette_optionsVCR.configurationなどを出力してみると良い

他のパターン

  • :pathを使えば違う環境(ドメイン)に対して動かすことができる
    • 例えば、http://stage1.example.com/, http://stage2.example.com/などの複数環境が混在している場合など

Custom VCR matchers

その他configurationオプション

vcrのcassettesの名前をカスタマイズする

rspecのexampleの文字をファイルのパスにする
RailsCasts #291 Testing with VCR から

RSpec.configure do |c|
  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

例えば spec/fixtures/vcr_cassettes/zip_code_lookup/show_beverly_hills_given_90210.yml のようなファイル名になる

ファイル名の文字数を制限する

gemの中でvcrを使っていたときに、そのgemをプロジェクトからbundleするとGem::Package::TooLongFileNameが出ることがあった
rubygemsでファイルの長さが100文字に制限されているらしい

ファイルを最大文字数でカットする

c.around(:each, :vcr) do |example|
    name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_")
    # NOTE Fix 'Gem::Package::TooLongFileName'
    name = name.split('/').map{|s| s[0..93] }.join('/')

StackOverflow : Building Rails 3 Engine Throwing Gem::Package::TooLongFileName Error

vcrのcassettesの名前にspec名を追加する

利点としては、特定のspecのcassettesを探したり削除しやすくなる

spec/features/foo/bar_spec.rbなら
spec/fixtures/vcr_cassettes/foo/bar/xxx_description_yyy.ymlのようになる

c.around(:each, type: :feature) do |example|
  spec_name = example.metadata[:file_path].gsub('./spec/features/', '').gsub('_spec.rb', '')
  desc_name = example.metadata[:full_description].split(/\s+/, 2).join("/").underscore.gsub(/[^\w\/]+/, "_")
  name = [spec_name, desc_name].join("/")

使われていないcassettesを削除する

Cleaning up unused cassettes

vcrをRAILS_ENV=developmentで使う

vcr_cable

vcr_cable

gem追加後、
$ bundle exec rails generate vcr_cable

config/vcr_cable.yml
development:
  hook_into: webmock
  cassette_library_dir: spec/fixtures/vcr_cassettes
  enable_erb: false
  allow_playback_repeats: false
  allow_http_connections_when_no_cassette: true
  enable_vcr_cable: true

有効にする方法は、enable_vcr_cable: trueにするか、ENABLE_VCR_CABLE=true bundle exec rails sでサーバーを起動する

意外にちゃんと動いた。あくまでモックの扱いなので、動的なアクション・データを扱いたい場合は使えなくなる。
spec/fixtures/vcr_cassettes/vcr_cable_cassette.ymlにカセットができるが、カセットが一つなのでファイルが肥大して問題になることもありそう。

他見つけたやつ。古い、試していない vcr-remote-controller

Rack::VCR

新しい
Rack::VCRでらくらくアプリケーション間テスト

rack-vcr
rack-vcr-sample

vcrのcassettes一覧を管理画面インターフェースで閲覧、削除する

mr_video

サブディレクトリに対応した模様
そのままだとcassette_library_dirの直下しか取り扱わない挙動
ディレクトリ配下を対象にする以下のIssueがある

Add Support for cassette_library_dir with nested folders

リクエストパラメータのArrayに括弧をつけない