gem VCRでweb mockを使う 

[gem vcr](https://github.com/vcr/vcr)を使ってpush通知のテストを効率化した話を書きます。

VCRとはテストで使う『HTTP通信』を1回目に記録しておいて、2回目以降のテストでの実行はその記録を使って行うことができます。
Webmockと組み合わせて使うと非常に効率的にテストができるようになりました。

## VCR
VCRの使い方の概要。

- テストコード内で実行するリクエストに、外部のwebAPIなどに対してHTTPリクエストを送る箇所があれば、そこにVCRを使う旨をまず設定する
- VCRでは設定した後にHTTPリクエストを送ると、設定ファイル(ymlファイル)に、HTTPリクエストとそれに対応するレスポンスの組合せを自動で記録し、モックを作成する。

VCRを有効にしてテストコードを実行すると、VCRは具体的には以下の動きになる。

1. テスト実行中に送信されるHTTPリクエストを監視
1. HTTPリクエストの送信を見つけると、設定ファイル内のHTTPメソッドとURIが一致する、HTTPリクエストを探索
1. HTTPリクエストが見つかれば、そこに記述されたHTTPレスポンスを返却
1. また、設定ファイル中に一致するHTTPリクエストが見つからなかった場合は例外を上げる。ゆえに、意図していないHTTPリクエストを送信したことが検出できる

## 導入
こちらの記事を参考にさせていただきました。
https://tech.actindi.net/2019/06/07/093705

- gem 
```Gemfile
gem 'vcr'
gem 'webmock'
```

- docker起動時のpumaコンテナに何かしらのAWSアクセスキーを設定
    - モックで動かす場合でも何かしらの値が環境変数に入っていなければいけないので、ローカルに環境変数がない人が実行した場合でも大丈夫なのように適当な文字列が入るようにしておく
    - ローカルでセットしている環境変数を取得し、セットされていなければ適当な文字列が入るようにする
    - AWSの環境変数は1回目のAPIコールの時だけ必要
```docker-compose.yml
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION: ap-northeast-1
AWS_DEFAULT_OUTPUT: json
```

- CI/CDを回すときにもVCRでのSDKの実行でコケるのでダミーで適当な文字列を入れておく必要がある
```.github/workflows/ci.yml
AWS_ACCESS_KEY_ID: AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY: AWS_SECRET_ACCESS_KEY
AWS_DEFAULT_REGION: ap-northeast-1
AWS_DEFAULT_OUTPUT: json
```

- rspecの設定
```spec/spec_helper.rb
# vcrの設定
require 'vcr'

VCR.configure do |config|
  config.cassette_library_dir = 'spec/support/vcr' # カセットを保存するルートディレクトリ
  config.hook_into :webmock # 利用するモックライブラリ（内部ではwebmockを利用しています）
  config.allow_http_connections_when_no_cassette = true # VCRを使わない場所ではHTTP通信を許可する
  config.default_cassette_options = { match_requests_on: [:method, :uri, :body] } # http通信時にカセットを探索する際のデフォルトのマッチ条件の指定※各テストケースでVCR.useするときに条件の上書きは可能
end
```

## VCRカセット作成方法
前提：
- カセットがなければ実際にAPIをコール（今回の　push通知でのテストで言うとAmazon SNSをコール）してそれを記録する
- 一連の処理で複数回のAPIコールがあれば、すべてひとつのymlファイルに記録される
- カセットがある場合は作成されているmockを返す

1. docker起動前に以下の環境変数をローカルにセットする

    ```
    export AWS_ACCESS_KEY_ID=
    export AWS_SECRET_ACCESS_KEY=
    ```

1. docker起動
   ```bash
   $ docker-compose down # 一度落としてから再起動すると反映は確実
   $ docker-compose up
   ```

1. APIリクエストを行うテストにカセットを設置
    ```
    VCR.use_cassette('requests/api/v1/cms/notifications/create/individual/200') do
      api_request
    end
   
    # リクエストの内容が不定の場合は match_requests_on を付ける
    VCR.use_cassette('requests/api/v1/cms/notifications/create/individual/200', match_requests_on: []) do
     api_request
    end
    ```
    ※ 記載したパスにカセットが生成される
1. テストを実行するとカセットが生成される
```yml: 例
---
http_interactions:
  - request:
      method: post
      uri: https://sns.ap-northeast-1.amazonaws.com/
      body:
        encoding: UTF-8
        string: Action=CreatePlatformEndpoint&CustomUserData=%7B%22user_uid%22%3A%22test_uid%22%7D&PlatformApplicationArn=platform_application_arn&Token=device_token&Version=2010-03-31
      headers:
        Content-Type:
          - application/x-www-form-urlencoded; charset=utf-8
        Accept-Encoding:
          - ''
        User-Agent:
          - aws-sdk-ruby3/3.114.0 ruby/2.7.3 x86_64-linux-musl aws-sdk-sns/1.41.0
        Host:
          - sns.ap-northeast-1.amazonaws.com
        X-Amz-Date:
          - 20211125T040015Z
        X-Amz-Content-Sha256:
          - hogehoge
        Authorization:
          - AWS4-HMAC-SHA256 Credential=AWS_ACCESS_KEY_ID/20211125/ap-northeast-1/sns/aws4_request,
            SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=hogehoge
        Content-Length:
          - '294'
        Accept:
          - "*/*"
    response:
      status:
        code: 200
        message: OK
      headers:
        X-Amzn-Requestid:
          - '028b4ae0-40ee-5e0d-83a8-10f94c61453d'
        Content-Type:
          - text/xml
        Content-Length:
          - '448'
        Date:
          - Thu, 25 Nov 2021 04:00:15 GMT
      body:
        encoding: UTF-8
        string: |
          <CreatePlatformEndpointResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
            <CreatePlatformEndpointResult>
              <EndpointArn>arn:aws:sns:ap-northeast-1:303975652040:endpoint/APNS_SANDBOX/hogehoge/hugahuga</EndpointArn>
            </CreatePlatformEndpointResult>
            <ResponseMetadata>
              <RequestId>028b4ae0-40ee-5e0d-83a8-10f94c61453d</RequestId>
            </ResponseMetadata>
          </CreatePlatformEndpointResponse>
    recorded_at: Thu, 25 Nov 2021 04:00:15 GMT
recorded_with: VCR 6.0.0

```

1. 生成されたカセットに `AWS_ACCESS_KEY_ID` が含まれている為文字列に置き換える（念の為）
1. endpoint_arnやdevice_tokenもリクエスト内に含まれていたらダミーの文字に置き換えておくとよい

## テスト手順

## テストケースを書く
- テストケースは通常通り期待動作を書く(request_spec,model_spec,job_specなど全て同様)

## VCRをセット
基本的にはAPIにリクエストを送る処理をVCRで囲む

- request_specの場合
```例:rb
subject(:api_request) do
  VCR.use_cassette('requests/api/v1/posts/comments/create/my_comment/200', match_requests_on: []) do
    authorization_mock
    post path, params: params, as: :json, headers: headers
  end
end
```

- job_specの場合
```例:rb
it '送信後に履歴が登録されていること' do
  VCR.use_cassette('jobs/send_push_notice_to_followers/manager_user/200', match_requests_on: []) do
    expect { exec }.to change(PushNoticeHistory, :count).by(2)
  end
end
```

## レコードモード

何も設定しなければデフォルトはonce

- ``once``

過去のものを再生できる
同名カセットファイルの記録は１度のみ
同名のカセットファイルがある場合はエラーを吐く

- ``new_episodes``

過去のものを再生できる
同名のカセットファイルがあれば追記する

- ``none``

過去のものを再生できる
新たな記録は行わない

- ``all``

過去に記録されててもとにかく全部記録する

- 設定例
``:record => :once``みたいな感じで設定する

## match_requests_on
- デフォルトのHTTPリクエストの一致条件は、HTTPリクエストのメソッド名とURI文字列

- GETリクエストの場合、メソッドとURIで一致を判定すれば、十分だが、POSTリクエストの場合、一致条件に「HTTPリクエスト Bodyの内容」を追加したいことがある

- Bodyを一致条件に追加すれば、POSTリクエストの送信内容が期待通りの情報が入っているかどうか確認できる

- ソースコード修正時に、意図せずに送信するHTTPリクエストのBody変更しても、テストで検出できるようになる

- なので、引数:``match_requests_on``でリクエストの一致条件では、なるべく絞っておいた方がテスト落ちを感知しやすい。

## パラメータの設定方法パターン
- bodyだけ指定
```
match_requests_on: [:body]
```
こう指定すると、一致条件からメソッドとURIはなくなり、ボディだけになる

- メソッドとURIとボディ全て指定
```
match_requests_on: [:method, :uri, :body]
```

- 条件を指定しない（リクエストボディやURIにリクエストのたびに異なる変数などが入る場合）
    - publish時のリクエストボディーに入るuidなど
```
match_requests_on: []
```

## グローバルに設定する方法

```
 VCR.configure do |config|
   config.default_cassette_options = {:record=>:once, :match_requests_on=>[:method, :uri, :body], :allow_unused_http_interactions=>true, :serialize_with=>:yaml, :persist_with=>:file_system}
 end
```

``:record=>:once``で、一度この設定をすれば、それ以降のすべての``VCR.use_cassette``では、設定したリクエスト一致条件が使われるようになる。

## 注意するケース
### レスポンスの内容を修正した場合
- レスポンスのymlを修正したい場合は元のファイルを一度削除してからカセットを実行しないとwebmockが作成されない。webmockを使っている場合はそちらが優先されるので不要であれば削除しておかないとAPIリクエストが走らないため。

### webmock化されていないhttpリクエストが処理の中に混じっていた場合

- 例えばsdkに実行させる処理を新規追加した場合はその処理のwebmockは作られていない。

- webmockを作らずメソッドを実行すると以下のようになってしまう
```ruby
(byebug) @client.get_endpoint_attributes(
      {
        endpoint_arn: target_arn
      }
    )
#<Class:0x00007f9880a07c90>
```

- 本来webmockが用意できていれば
```ruby
(byebug) @client.get_endpoint_attributes(
      {
        endpoint_arn: target_arn
      }
    )
#<struct Aws::SNS::Types::GetEndpointAttributesResponse attributes={"Enabled"=>"true", "Token"=>"43750d9d42d93e8c43a1d87ebd9d3424bd435892b696c37c97a3b7f43df32832", "CustomUserData"=>"{\"user_uid\":\"5mylaFYVIvbVS2gf\"}"}>
```

ゆえに、webmockがなくても、SDKの処理自体で例外を出すわけではなく、通常のmockのインスタンスをデフォルトで作ってしまうので、その後のテストが落ちなければテストケース漏れに気づきにくいため、気をつける必要がある。



#　終わりに

株式会社Relicでは、サーバーサイドエンジニアを積極的に採用中です。
またRelicでは、地方拠点がありますので、U・Iターンも大歓迎です！🙌
少しでもご興味がある方は、Relic採用サイトからエントリーください！

https://relic.co.jp/categories/job_3?utm_source=qiita&utm_medium=article&utm_campaign=qiita_article