gem vcrを使ってpush通知のテストを効率化した話を書きます。
VCRとはテストで使う『HTTP通信』を1回目に記録しておいて、2回目以降のテストでの実行はその記録を使って行うことができます。
Webmockと組み合わせて使うと非常に効率的にテストができるようになりました。
VCR
VCRの使い方の概要。
- テストコード内で実行するリクエストに、外部のwebAPIなどに対してHTTPリクエストを送る箇所があれば、そこにVCRを使う旨をまず設定する
- VCRでは設定した後にHTTPリクエストを送ると、設定ファイル(ymlファイル)に、HTTPリクエストとそれに対応するレスポンスの組合せを自動で記録し、モックを作成する。
VCRを有効にしてテストコードを実行すると、VCRは具体的には以下の動きになる。
- テスト実行中に送信されるHTTPリクエストを監視
- HTTPリクエストの送信を見つけると、設定ファイル内のHTTPメソッドとURIが一致する、HTTPリクエストを探索
- HTTPリクエストが見つかれば、そこに記述されたHTTPレスポンスを返却
- また、設定ファイル中に一致するHTTPリクエストが見つからなかった場合は例外を上げる。ゆえに、意図していないHTTPリクエストを送信したことが検出できる
導入
こちらの記事を参考にさせていただきました。
https://tech.actindi.net/2019/06/07/093705
- gem
gem 'vcr'
gem 'webmock'
- docker起動時のpumaコンテナに何かしらのAWSアクセスキーを設定
- モックで動かす場合でも何かしらの値が環境変数に入っていなければいけないので、ローカルに環境変数がない人が実行した場合でも大丈夫なのように適当な文字列が入るようにしておく
- ローカルでセットしている環境変数を取得し、セットされていなければ適当な文字列が入るようにする
- AWSの環境変数は1回目のAPIコールの時だけ必要
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の実行でコケるのでダミーで適当な文字列を入れておく必要がある
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の設定
# 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を返す
-
docker起動前に以下の環境変数をローカルにセットする
export AWS_ACCESS_KEY_ID= export AWS_SECRET_ACCESS_KEY=
-
docker起動
$ docker-compose down # 一度落としてから再起動すると反映は確実 $ docker-compose up
-
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
※ 記載したパスにカセットが生成される
-
テストを実行するとカセットが生成される
---
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
- 生成されたカセットに
AWS_ACCESS_KEY_ID
が含まれている為文字列に置き換える(念の為) - endpoint_arnやdevice_tokenもリクエスト内に含まれていたらダミーの文字に置き換えておくとよい
テスト手順
テストケースを書く
- テストケースは通常通り期待動作を書く(request_spec,model_spec,job_specなど全て同様)
VCRをセット
基本的にはAPIにリクエストを送る処理をVCRで囲む
- request_specの場合
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の場合
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
過去のものを再生できる
同名カセットファイルの記録は1度のみ
同名のカセットファイルがある場合はエラーを吐く
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を作らずメソッドを実行すると以下のようになってしまう
(byebug) @client.get_endpoint_attributes(
{
endpoint_arn: target_arn
}
)
#<Class:0x00007f9880a07c90>
- 本来webmockが用意できていれば
(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採用サイトからエントリーください!