VCRで外部APIダンプを作って使う


外部APIのアクセスをするライブラリとか作成するとき、
毎回、APIをコールせず済むようになります.

今回、試したのは以下の環境です.

- Rails 4.2
- Rspec 3.2
- [WebMock](https://github.com/bblimke/webmock)
- [VCR](https://github.com/vcr/vcr)

## ライブラリ

### WebMock

WebMockは名前の通りhttp(s)のモックです.
これでHTTP通信を差し替えます.

### VCR

VCRはモックやスタブのデータを実際のHTTP通信から記録するライブラリです.
多分、ビデオカセットレコーダー(Video Cassette Recorder)の略

## テスト対象のクラス

テスト対象として以下の様なAPIアクセスのクラスを作ったとします.
これのテストを書いていきます.

```ruby:example_api.rb
require 'net/http'
require 'json'

class ExampleApi
  STATUS_URL = 'https://example.com/contents/status'
  
  def initialize(provider_id, api_key)
    @provider_id = provider_id
    @api_key = api_key
  end

  def status(content_ids)
    uri = URI.parse STATUS_URL
    uri.query = { pid: @provider_id, api_key: @api_key }

    res = post_http uri,
                    headers: { 'Content-Type' => 'application/json' },
                    body: { content_ids: content_ids }.to_json

    return JSON.parse(res.body, symbolize_names: true) if res.code == '200'
    err = JSON.parse res.body
    fail err['message']
  end

  def post_http(uri, options = {})
    ...
  end
end
```

## 導入

とりあえず幾つか設定する必要があります.
`Gemfile`と`spec/rails_helper.rb`に以下を追記します.

```ruby:Gemfile
group :development, :test do
  # web mock
  gem 'webmock', require: false
  gem 'vcr', require: false
end
```

```ruby:spec/rails_helper.rb
require 'webmock/rspec'
require 'vcr'

VCR.configure do |c|
  c.cassette_library_dir = 'spec/vcr'
  c.hook_into :webmock
  c.configure_rspec_metadata!
end
```

APIのキーは`secrets.yml`で管理しているので編集しておきます.

```yaml:config/secrets.yml
test:
  example_provider_id: <%= ENV['EXAMPLE_PROVIDER_ID'] %>
  example_api_key: <%= ENV['EXAMPLE_API_KEY'] %>
```

## テストコード

テストコードはこんな感じになります.

`c.configure_rspec_metadata!`が指定してあると、`describe`や`context`のメタデータで`vcr: true`を指定すると初回時にHTTP通信をして記録します.

２回目以降は記録したHTTPのダンプデータを使うようになります.

```ruby:example_api_spec.rb
describe ExampleApi do
  subject { described_class.new provider_id, api_key }
  let(:provider_id) { Rails.application.secrets.example_provider_id }
  let(:api_key)     { Rails.application.secrets.example_api_key }

  describe '#status' do
    subject { super().status content_ids }

    # VCRを有効にいする
    context 'success', vcr: true do
      let(:content_ids) { %w(test_1 test_2) }

      it do
        should match(
          [
            { content_id: 'test_1', status: 'RUNNING' },
            { content_id: 'test_2', status: 'FINISHED' }
          ]
        )
      end
    end
  end
end
```

## 機密情報のフィルタリング

上記のテストでは`Rails.application.secrets`を使っているのでVCRで記録したときに`spec/vcr`以下に機密情報が含まれたyamlが作成されています.

このままではGit等のSCMで管理しにくいですね.
そこで機密情報をフィルタリングしていきます.

元ネタは以下にありました.

- [ruby - Filtering sensitive data with VCR - Stack Overflow](http://stackoverflow.com/questions/9816152/filtering-sensitive-data-with-vcr)


`config/secrets.yml`を編集します.
テスト環境に環境変数を指定するのもいいですが、今回はデフォルト値で置き換えてしまいます.

```yaml:config/secrets.yml
test:
  example_provider_id: <%= ENV['EXAMPLE_PROVIDER_ID'] || 'example_pid' %>
  example_api_key: <%= ENV['EXAMPLE_API_KEY'] || 'example_api_key' %>
```

そしてVRCのフィルタリングは`filter_sensitive_data`を使います.
今回はRailsの`config/secrets.yml`を使っているので以下のようにしてみました.

```ruby:spec/rails_helper.rb
VCR.configure do |c|

  ...

  Rails.application.secrets.each do |k, v|
    next unless v.is_a? String
    c.filter_sensitive_data("<#{k.upcase}>") { v }
  end
end
```

## ローカルホストの接続を除外

CucumberやTurnipを使ってるときローカルホストを除外したい時があります.
`ignore_localhost`を指定することで除外できます.

```ruby:spec/rails_helper.rb
VCR.configure do |c|

  ...

  c.ignore_localhost = true
end
```

## 感想

思ったより手軽に実装できました.

~~自分の場合、いろいろ検討した結果RSpecを使い続けることにしました.~~

RSpecで変態的なテストコードも書けますが、できる限りシンプルにテストを実装できるようにしています.

ライブラリ等のテストは使い方も兼ねるように書き書くため、RSpecだとsubjectにまとめるようにしています.