[RSpec][VCR] WEB API呼び出しのテストをstubしてみる(その2)
- 2015年06月03日
- CATEGORY- 1. 技術力{技術情報}
松山です。
前回の続きです。ソースコードに色々な肉付けを施したいと思います。
先ずはおさらいから。
【前回やったこと】
・お天気webサービス(http://weather.livedoor.com/weather_hacks/webservice)を呼び出す処理の簡単なテストコードを書く
・テスト実行で通信が発生しないようVCRを利用してwebサービスの呼び出しをstubする
・テストコードがVCRに依存しない仕組みを作る
【今回やること】
・お天気webサービスを呼び出して何かしらの処理を行う
・何かしらの処理を行うメソッドのテストコードを書く
・考察と施策
参考文献
・VCR(https://github.com/vcr/vcr)
・VCR – Relish(https://relishapp.com/vcr/vcr/v/2-9-3/docs)
環境
・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 (NEW!)
・spring-commands-rspec 1.0.2 (NEW!)
前回までのソースコードは次の通りです。
# 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
# app/models/weather.rb class Weather < ActiveResource::Base self.site = 'http://weather.livedoor.com/' self.prefix = '/forecast/webservice/json' self.format = :json self.include_format_in_path = false self.element_name = '' def self.forecast(params) self.new get("v1", params) end end
# spec/models/weather_spec.rb describe Weather, type: :model do describe 'forecast' do subject { Weather.forecast(params) } context 'city-id is 020020' do let(:params) { { city: '020020' } } - describe '.title' do + describe '#title' do it { expect(subject.title).to eq '青森県 むつ の天気' } end end end end # 実行結果 $ spring rspec spec/models/weather_spec.rb Weather forecast city-id is 020020 .title should eq "青森県 むつ の天気" Finished in 0.02988 seconds (files took 6 minutes 21 seconds to load) 1 example, 0 failures
【何かしらの処理を行う】
お天気webサービスのresponseを解釈して何かしらの処理を行うメソッドをモデルに追加します。おっと先ずはテストコードから。
# spec/models/weather_spec.rb describe Weather, type: :model do describe 'forecast' do subject { Weather.forecast(params) } context 'city-id is 020020' do let(:params) { { city: '020020' } } describe '#title' do it { expect(subject.title).to eq '青森県 むつ の天気' } end + + describe '#location_names' do + it { expect(subject.location_names).to match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"] } + end end end end
地名を返すlocation_namesメソッドのテストコードです。ここでは配列要素の順序も仕様としてチェックするものとします。
# 実行結果 $ spring rspec spec/models/weather_spec.rb Weather forecast city-id is 020020 #title should eq "青森県 むつ の天気" #location_names example at ./spec/models/weather_spec.rb:15 (FAILED - 1) Failures: 1) Weather forecast city-id is 020020 #location_names Failure/Error: it { expect(subject.location_names).to match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"] } NoMethodError: undefined method `location_names' for #<Weather:0x007fec2ed17c58>
これをgreenにするコードを書いてみます。
# app/models/weather.rb class Weather < ActiveResource::Base self.site = 'http://weather.livedoor.com/' self.prefix = '/forecast/webservice/json' self.format = :json self.include_format_in_path = false self.element_name = '' def self.forecast(params) self.new get("v1", params) end + + def location_names + pinpointLocations.map &:name + end end
$ spring rspec spec/models/weather_spec.rb Weather forecast city-id is 020020 #title should eq "青森県 むつ の天気" #location_names should match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"] Finished in 0.02007 seconds (files took 13.06 seconds to load) 2 examples, 0 failures
うん、キモチイイ(←題材が簡単なのでなんですが、このような小さい快楽の積み重ねがモチベーションの継続に繋がりますよね。非常に重要なファクターだと思うのです)
【考察と施策】
前回の続きなので、お天気webサービスを実際には呼び出しておらず、VCRのカセットをplaybackして期待する結果を検証しています。ですがもし、オトナの事情により、
期待値が [“むつ市”, “大間町”, “東通村”, “風間浦村”, “佐井村”]ではなく [“大間町”, “むつ市”, “東通村”, “風間浦村”, “佐井村”]に変わったとしたら、お天気webサービスの仕様変更をカセットが隠蔽し続けることになります。
これはイケナイ状況です。そこで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.default_cassette_options = { record: :new_episodes, :re_record_interval => 1.second } 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
1秒は極端な例ですが re_record_interval を追記しました。VCRのカセットは自身の作成日付を保持していて、もし陳腐化しているのなら再作成を促すコードが書けるのです。
実運用であれば、任意の環境変数に値を設定したらカセットの再作成、という方針も取れます。
しばらくVCRを触ってみて解ったのですが、最初の印象より成熟したプロダクトです。やりたいことは大抵できます。素晴らしい。
次回もVCRをいー感じに紹介できたらと思いまっす。
うちのペットです。稀にこんな感じで出迎えてくれます。こうなると、今日どこまで掃除したんだ?と気になってしまいますが、可愛いペットなので愛でるようにステーションへdockさせます。
最近は和製のお掃除ロボットが台頭していますが、ちょっと旧式で憎めないこの子と添いたげたいと思います。
では、また。
【関連記事】
MSXとKONAMIさん
MSXとKONAMIさん(その2)
MSXとKONAMIさん(その3)
[RSpec][VCR] WEB API呼び出しのテストをstubしてみる
- 2015年06月03日
- CATEGORY- 1. 技術力{技術情報}