KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

[RSpec][VCR] WEB API呼び出しのテストをstubしてみる(その3)


松山です。

前回の続きです。先ずはおさらいから。

【前回やったこと】
・お天気webサービスを呼び出して何かしらの処理を行う
・何かしらの処理を行うメソッドのテストコードを書く
・考察と施策

【今回やること】
・VCRがきちんと働いているかを確認するためのコードを仕込む

参考文献
・VCR(https://github.com/vcr/vcr)
・VCR – Relish(https://relishapp.com/vcr/vcr/v/2-9-3/docs)
・Module: VCR — Documentation for vcr (2.9.3) – (http://www.rubydoc.info/gems/vcr/VCR)

環境
・ruby 2.1.3p242 (2014-09-19 revision 47630) [x86_64-darwin13.0]
・rspec 3.1.7
・rails 4.1.6
・vcr 2.9.3
・webmock 1.20.4
・spring 1.2.0
・spring-commands-rspec 1.0.2

【VCRがきちんと働いているかを確認するためのコードを仕込む】
前回までのソースですと、テスト実行時に通信が生じた際にVCRが反応してくれてるのかが解りにくいです。ですのでデバッグ用のコードを仕込みたいと思います。
ちなみにVCRは標準でこの機能を持っています。有効にするには以下コードの、

# spec/spec_helper.rb
RSpec.configure do |config|
  VCR.configure do | c |
    c.allow_http_connections_when_no_cassette = true
    c.hook_into :webmock
    c.cassette_library_dir = 'spec/vcr_cassettes'
    c.default_cassette_options = { record: :new_episodes }
    c.before_record { | i | i.response.body.force_encoding 'UTF-8' }
    c.around_http_request do | request |
      VCR.use_cassette(request.parsed_uri.path, &request)
    end
    # c.debug_logger = $stdout
  end
end

”# c.debug_logger = $stdout” のコメントアウトを外せば良いです。それでテストを実行してみると・・・

$ spring rspec spec/models/weather_spec.rb

Weather
  forecast
    city-id is 020020
      #title
[webmock] Handling request: [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] (disabled: false)
[Cassette: '/forecast/webservice/json/v1'] Initialized with options: {:record=>:new_episodes, :match_requests_on=>[:method, :uri], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system}
  [Cassette: '/forecast/webservice/json/v1'] Initialized HTTPInteractionList with request matchers [:method, :uri] and 1 interaction(s): { [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] => [200 "{\"pinpointLocations\":[{\"link\":\"http://weather.livedoor.com/area/forecast/0220800"] }
  [Cassette: '/forecast/webservice/json/v1'] Checking if [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] matches [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] using [:method, :uri]
    [Cassette: '/forecast/webservice/json/v1'] method (matched): current request [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] vs [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
    [Cassette: '/forecast/webservice/json/v1'] uri (matched): current request [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] vs [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
  [Cassette: '/forecast/webservice/json/v1'] Found matching interaction for [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] at index 0: [200 "{\"pinpointLocations\":[{\"link\":\"http://weather.livedoor.com/area/forecast/0220800"]
[webmock] Identified request type (stubbed_by_vcr) for [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
        should eq "青森県 むつ の天気"
      #location_names
[webmock] Handling request: [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] (disabled: false)
[Cassette: '/forecast/webservice/json/v1'] Initialized with options: {:record=>:new_episodes, :match_requests_on=>[:method, :uri], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system}
  [Cassette: '/forecast/webservice/json/v1'] Initialized HTTPInteractionList with request matchers [:method, :uri] and 1 interaction(s): { [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] => [200 "{\"pinpointLocations\":[{\"link\":\"http://weather.livedoor.com/area/forecast/0220800"] }
  [Cassette: '/forecast/webservice/json/v1'] Checking if [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] matches [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] using [:method, :uri]
    [Cassette: '/forecast/webservice/json/v1'] method (matched): current request [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] vs [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
    [Cassette: '/forecast/webservice/json/v1'] uri (matched): current request [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] vs [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
  [Cassette: '/forecast/webservice/json/v1'] Found matching interaction for [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020] at index 0: [200 "{\"pinpointLocations\":[{\"link\":\"http://weather.livedoor.com/area/forecast/0220800"]
[webmock] Identified request type (stubbed_by_vcr) for [get http://weather.livedoor.com/forecast/webservice/json/v1?city=020020]
        should match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"]

Finished in 0.03022 seconds (files took 2.41 seconds to load)
2 examples, 0 failures

一気に可読性が下がりました(泣)これは、どの条件でCassette内を走査してマッチングを行ったかの過程と結果が細かに出力されています。この子はこれで必要な情報なのですが、普段の開発では少し情報量が多すぎますね。テストの網羅性を直感的に視認したいときはノイズになってしまいます。ですので自前で実装してみましょう。

# spec/spec_helper.rb
RSpec.configure do |config|
  VCR.configure do | c |
    c.allow_http_connections_when_no_cassette = true
    c.hook_into :webmock
    c.cassette_library_dir = 'spec/vcr_cassettes'
    c.default_cassette_options = { record: :new_episodes }
    c.before_record { | i | i.response.body.force_encoding 'UTF-8' }
    c.around_http_request do | request |
      VCR.use_cassette(request.parsed_uri.path, &request)
    end
+   ['recordable', 'stubbed'].each do | method |
+     c.after_http_request("#{method}?".to_sym) do | request, _response |
+       puts "- VCR - #{method} - [#{request.method}] #{request.parsed_uri}"
+       puts "  used cassette - #{VCR.current_cassette.file}"
+     end
+   end
    # c.debug_logger = $stdout
  end
end

テストを実行してみます。

$ spring rspec spec/models/weather_spec.rb

Weather
  forecast
    city-id is 020020
      #title
- VCR - stubbed - [get] http://weather.livedoor.com/forecast/webservice/json/v1?city=020020
  used cassette - /xxx/spec/vcr_cassettes/forecast/webservice/json/v1.yml
        should eq "青森県 むつ の天気"
      #location_names
- VCR - stubbed - [get] http://weather.livedoor.com/forecast/webservice/json/v1?city=020020
  used cassette - /xxx/spec/vcr_cassettes/forecast/webservice/json/v1.yml
        should match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"]

Finished in 0.02309 seconds (files took 19 minutes 24 seconds to load)
2 examples, 0 failures

(”xxx”は伏字です)次にCassetteファイルを削除してテスト実行します。

$ rm /xxx/spec/vcr_cassettes/forecast/webservice/json/v1.yml
$ spring rspec spec/models/weather_spec.rb

Weather
  forecast
    city-id is 020020
      #title
- VCR - recordable - [get] http://weather.livedoor.com/forecast/webservice/json/v1?city=020020
  used cassette - /xxx/spec/vcr_cassettes/forecast/webservice/json/v1.yml
        should eq "青森県 むつ の天気"
      #location_names
- VCR - stubbed - [get] http://weather.livedoor.com/forecast/webservice/json/v1?city=020020
  used cassette - /xxx/spec/vcr_cassettes/forecast/webservice/json/v1.yml
        should match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"]

Finished in 0.08782 seconds (files took 28 minutes 57 seconds to load)
2 examples, 0 failures

今度は一回目のリクエストでCassetteを作成(recordable)し、2回目でそれを再生(stubbed)している様子が解りやすいです。Cassetteのファイル名が解るのもいいですね。開発者はこれを見て、stub漏れがないことを確認できます。

今回はここまで!

matsuyama01
55インチでのプログラミングを試みた時の写真です。買ったことを黒歴史にしないためにもApple TVを利用してみました。しかし、恐らく1秒未満ではありますがキープレス後のタイムラグがありまして、それに耐え切れず挫折ですw

 

【関連記事】
MSXとKONAMIさん
MSXとKONAMIさん(その2)
MSXとKONAMIさん(その3)
[RSpec][VCR] WEB API呼び出しのテストをstubしてみる
[RSpec][VCR] WEB API呼び出しのテストをstubしてみる(その2)