「APIのテスト書きたいけど、どんな感じでかけばいいのか忘れちゃった!」というときに、とりあえずこのテンプレをコピペしてからコーディングを始めることで超絶時短を狙う。
テンプレート
前提
FactoryBot利用し、Factoryのクラスメソッド実行時にFactory.
を省略できるような設定をrails_helperでしている
require 'rails_helper'
RSpec.describe "/<対象リソース名(スネークケース、複数形)>", type: :request do
# xxx_xxxテーブルのレコード設定
## <レコードの説明>
let!(:xxx_xxx1) { create(:<xxx_xxx>, :<trait>,...) }
## <レコードの説明>
let!(:xxx_xxx2) { create(:<xxx_xxx>, :<trait>,...) }
...
# yyy_yyyテーブルのレコード設定
## <レコードの説明>
let!(:yyy_yyy1) { create(:<yyy_yyy>, :<trait>,...) }
## <レコードの説明>
let!(:yyy_yyy2) { create(:<yyy_yyy>, :<trait>,...) }
...
# APIリクエスト関連
## 認証トークン
let!(:token) do
<認証処理>
end
## リクエストヘッダ(認証OK)
let!(:valid_headers) { { 'Content-Type' => 'application/json', 'Authorization' => "Token #{token}" } }
## リクエストヘッダ(認証NG)
let!(:valid_headers) { { 'Content-Type' => 'application/json', 'Authorization' => "Token #{token}a" } }
## ステータスコード
let(:res_status) { response.status }
## レスポンスボディ(のJSON文字列をパースして、キーがシンボルであるハッシュとして受け取ったもの)
let(:res_body) { JSON.parse(response.body, { symbolize_names: true }) }
## 認証エラー時のレスポンスボディ
let!(:res_body_unauthorized) { { error: 'unauthorized' } }
describe '<HTTPメソッド> /<アクション>' do
context '正常系' do
# リクエストパラメータ
let!(:params) { { a: 1, b: 2,... }.to_json }
# リクエストヘッダ
let!(:headers) { valid_headers }
# リクエスト(GET /index時)
subject { get <リソース名(スネークケース,複数形)>_url, headers: headers }
# リクエスト(GET /show時)
# subject { get <リソース名(スネークケース,単数形)>_url(<表示対象のリソースのモデルインスタンス(ActiveRecord)>), params: params, headers: headers }
# リクエスト(POST /create時)
# subject { post <リソース名(スネークケース,複数形)>_url, params: params, headers: headers }
# リクエスト(PUT /update時)
# subject { put <リソース名(スネークケース,単数形)>_url(<更新対象のリソースのモデルインスタンス(ActiveRecord)>), params: params, headers: headers }
# リクエスト(DELETE /delete時)
# subject { delete <リソース名(スネークケース,単数形)>_url(<削除対象のリソースのモデルインスタンス(ActiveRecord)>), headers: headers }
let(:expected_body) do
{
<期待するレスポンスボディの内容>
}
end
it 'ステータスコード: <ステータスコード>, ボディ: <レスポンスボディの概要>, DB: <DBデータの変化概要>' do
# リクエスト実行
expect(subject).to change(<リソースのクラス名>, :size).by(<変化したレコード数>)
# ステータスコードチェック
expect(res_status).to eq(<ステータスコード>)
# レスポンスボディチェック
expect(res_body).to eq(expected_body)
end
end
context '異常系: 認証エラー' do
# リクエストパラメータ
let!(:params) { { a: 1, b: 2,... }.to_json }
# リクエストヘッダ
let!(:headers) { invalid_headers }
# リクエスト(GET /index時)
subject { get <リソース名(スネークケース,複数形)>_url, headers: headers }
it 'ステータスコード: <ステータスコード>, ボディ: <レスポンスボディの概要>, DB: レコードが作成されない' do
# リクエスト実行
expect(subject).to change(<リソースのクラス名>, :size).by(0)
# ステータスコードチェック
expect(res_status).to eq(401)
# レスポンスボディチェック
expect(res_body).to eq(res_body_unauthorized)
end
end
context '異常系: リクエストパラメータ不備' do
end
...
end
end
補足
コードの構造にルールを設け、シンプルに保つことを心がける。
①階層構造(describe, context, it)
-
**「チェック対象はdescribe, 条件はcontext, 期待結果はit」**と階層化する。
-
DBのデータやリクエスト、レスポンスに関する定義など、準備はdescribe, contextで済ませておく
-
itでは、
リクエストを投げる → それによる結果をチェックする
に注力する
②準備系の設定(let!, let, before)
後の階層で変数的に使いまわしたい設定は、let!での定義を基本とし、時間差が必要な場合のみletを使う
※letは、遅延評価をしっかり意識しないと期待しない挙動をしてしまい、泥沼にハマる可能性が高いため
※後から参照しなくてよい設定は、beforeで定義する慣習らしい。・・・が、コード書いてる内に後から「やっぱり必要でした!」というケースもあるし、let!でなくbeforeを使うメリットはさほどないと判断
③リクエストのスローとレスポンスのチェック(subject, response)
- subjectに、実際にリクエストを投げる処理を記述する
- subject内で記述する**APIのパスは、
<bundle exec rails routeで表示されるパスの省略系>_url
**でベタ書きせず書く - 2.について、show, update, delete系のAPIは、その引数に操作対象リソースのモデルインスタンスを記述(
xxx_url(<インスタンス>)
)してやるとidをベタ書きせずに書ける - API実行結果であるレスポンスボディ、ステータスコードはresponseから参照する(response.status, response.body)
※subjectを使用してリクエストをスローすると、responseでその結果を受け取れる、というrspecの機能
があるのでこれを利用 - レスポンスボディ(response.body)は、JSON文字列でありチェック時に扱いづらいため、キーはシンボルでハッシュ化する処理を定義しておく
- 期待するレスポンスボディの定義も、キーがシンボルなハッシュで記述して、3.と突き合わせる形でチェックする
- ステータスコードのチェックは、数値でベタ書きする(be_successとか書くと、200でも201でもチェック通過してしまい、曖昧性が排除できない)
- レスポンスのチェックは、「ステータスコード」「レスポンスボディ」「DBのレコード数変化」の3点とする。
※レコードの内容がなにかしら変化する系のアクション(create, update, delete)は、「DBの中身的にもホントに期待する結果になっているのか?」まで見たほうが良い気もするが、個数だけチェックしている記事が多いっぽいのでそうする(腑に落ちてはいない)。
メモ
ホントはheaderの定義もキーがシンボルのハッシュで書きたい。
しかし、なぜかContent-type:
がうまくかけない。
なので、渋々キーが文字列のハッシュにした。