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

KEIS BLOG

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


松山です。
前回の続きです。ソースコードに色々な肉付けを施したいと思います。
先ずはおさらいから。

【前回やったこと】
・お天気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をいー感じに紹介できたらと思いまっす。

matsuyama01

うちのペットです。稀にこんな感じで出迎えてくれます。こうなると、今日どこまで掃除したんだ?と気になってしまいますが、可愛いペットなので愛でるようにステーションへdockさせます。
最近は和製のお掃除ロボットが台頭していますが、ちょっと旧式で憎めないこの子と添いたげたいと思います。

では、また。
 

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