[VCR][RSpec] WebMock.stub_requestに失敗していないかを簡単に確かめたい


###概要
WebMock.stub_requestはURIのマッチングに正規表現が使えるというデキる子ですが、VCRと一緒にテストを書いていると意図せずstubに失敗し、裏でこっそりとCassetteが自動生成されるという事態に遭遇することがあります（[[VCR][RSpec]VCRに依存しないテストコードを書く](http://qiita.com/choripon/items/44b92f30f3196e485b51)のように、テストコードにuse_cassetteを書かなくて済む設定がある場合）。この時、テストの実行結果からこれらの挙動が読み取れないため不便です。

###環境
* ruby 2.1.3p242 (2014-09-19 revision 47630) [x86_64-darwin13.0]
* rspec 3.1.7
* vcr 2.9.3

###やること
* テストの実行時に「Cassetteに書き込んだのか」「Cassetteを使ってstubしたのか」「WebMockによるstubなのか」を標準出力する
* テストの実行時にアクセスしたCassetteのファイルパスを標準出力する

ちなみにVCR標準の[Debug Logging](https://www.relishapp.com/vcr/vcr/v/2-9-3/docs/configuration/debug-logging)も用意されていますが、こちらは情報量が多すぎてノイジーなので自分はあまり使っていません。

```spec/spec_helper.rb
VCR.configure do | c |
  c.allow_http_connections_when_no_cassette = true
  c.hook_into :webmock
  c.cassette_library_dir = "spec/vcr_cassettes"
  c.configure_rspec_metadata!
  c.default_cassette_options = { erb: true, record: :new_episodes }

  c.before_record do | i |
    i.response.body.force_encoding "UTF-8"
    i.response.body = JSON.pretty_generate(JSON.parse(i.response.body))
  end

  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}"
      unless request.externally_stubbed?
        puts "  used cassette - #{VCR.current_cassette.try(:file)}"
      else
        puts "  stubbed by WebMock"
      end
    end
  end
  
  # c.debug_logger = $stdout
end
```

過去記事の設定が混じっていますが、今回のメインは[ "recordable", "stubbed" ].each do | method |〜のブロックです。

###[お天気WEBサービス](http://weather.livedoor.com/weather_hacks/webservice)を呼び出すテストを実行してみます。

モデル

```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
```

テストコード

```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', :vcr_erb => {:test_name => "{a:1}"} do
        it { expect(subject.title).to eq '青森県 むつ の天気' }
      end

      describe '#location_names' do
        it { expect(subject.location_names).to match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"] }
      end
    end

    context 'city-id is 020030' do
      before do
        uri = URI.parse("#{Weather.site}#{Weather.prefix}/v1?city=020030").to_s
        stub_request(:get, uri).to_return(body: '{"pinpointLocations":{}}')
      end

      let(:params) { { city: '020030' } }

      it { expect(subject).to be_present }
    end
  end
end
```

テスト実行

```bash
$ 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 - /(ないしょ)/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 - /(ないしょ)/spec/vcr_cassettes/forecast/webservice/json/v1.yml
        should match ["むつ市", "大間町", "東通村", "風間浦村", "佐井村"]
    city-id is 020030
- VCR - stubbed - [get] http://weather.livedoor.com/forecast/webservice/json/v1?city=020030
  stubbed by WebMock
      should be present

Finished in 0.14188 seconds (files took 62 minutes 51 seconds to load)
3 examples, 0 failures
```

３つの"- VCR -"が出力されています。

* １つ目は"recordable"で、#titleのテスト実行により[get]〜のURIに実通信が生じ、その内容を"used cassette"が示すファイルに記録したことを表しています。
* ２つ目は"stubbed"で、先ほど自動生成したCassetteを再利用して実通信をstubしたことが分かります。
* ３つ目も"stubbed"ですが、こちらはWebMockによりstubされたことを表しています。

これで、stub_requestが開発者の意図通りに動作していない場合、それに気付きやすくなります。withやheadersがちょっと違ってstub漏れするマイあるある。