この記事はトレタ Advent Calendar 2016の8日目です。
それなりの規模のサービスだとテストを書くと思う。 テストコードはテストデータの準備なども含めると、コード量が多くなりやすい。そのため、読みやすく意図が通じやすいテストを書くように意識しないと初めてテストコードを読む人が理解し辛い。
テストを書くときに考えていることを言語化して他の人と議論したことが無かったため、今回のエントリでは普段自分が気をつけていることについて書いてみる。
例として、予約を更新する PUT /reservations/:id
について述べる。リクエストを受け取ったらJSONを返すAPIとする。
describe "ReservationsController" do describe "PUT /reservations/:id" do let(:reservation) do FactoryGirl.create(:reservation, id: "bb9e49aed217035ddb56962c0685a966", seats: 2, name: "m_nakamura145", staff_id: "3a3c0ee3c33c96f886d5b1ad32ee44cf", plan: 'lunch') end let(:params) do { id: "bb9e49aed217035ddb56962c0685a966", seats: 5, name: "m_nakamura333", staff_id: "8c2d5ae49f9d66da2493077790e04586", plan: "dinner" } end context "when all params are updated" do it "returns 200" do response = put :update, params body = JSON.parse(response.body) expect(response.status).to eq(200) expect(body['seats']).to eq(params[:seats]) expect(body['name']).to eq(params[:name]) expect(body['staff_id']).to eq(params[:staff_id]) expect(body['plan']).to eq(params[:plan]) end end context "when seats is updated with max seats" do let(:params) do { id: "bb9e49aed217035ddb56962c0685a966", seats: 20, name: "m_nakamura145", staff_id: "3a3c0ee3c33c96f886d5b1ad32ee44cf", plan: "lunch" } end it "returns 200" do response = put :update, params body = JSON.parse(response.body) expect(response.status).to eq(200) expect(body['seats']).to eq(params[:seats]) end end context "when seats is updated with min seats" do let(:params) do { id: "bb9e49aed217035ddb56962c0685a966", seats: 1, name: "m_nakamura145", staff_id: "3a3c0ee3c33c96f886d5b1ad32ee44cf", plan: "lunch" } end it "returns 200" do response = put :update, params body = JSON.parse(response.body) expect(response.status).to eq(200) expect(body['seats']).to eq(params[:seats]) end end context "when id is deleted" do before do reservation.destroy end it "returns 404" do response = put :update, params expect(response.status).to eq(404) end end context "when seats is updated with minus seats" do let(:params) do { id: "bb9e49aed217035ddb56962c0685a966", seats: -1, name: "m_nakamura145", staff_id: "3a3c0ee3c33c96f886d5b1ad32ee44cf", plan: "lunch" } end it "returns 400" do response = put :update, params expect(response.status).to eq(400) end end end end
上のようなテストを書くときに以下を意識している。
テストファイル内のテストを書く順番は正常系->異常系にする
テストファイルの中の上部に正常系が集まっていて、下部に異常系が集まっていると挙動が把握しやすい。 特に、最も基本となる挙動のテストが一番上に書かれていると、初めてコードを読んだ人が基本の挙動をすぐに押さえることができるので良い。
対応関係を明示する
context
に「このケースはこういう状態のテストです」と必ず書く。
context
を書かずにit
の中でテストデータを作るテストを書く人もいるが、そのテストが何の状態にフォーカスしているのかが分かりづらいと感じるので、context
は必ずテストデータの状態について書いている。
正常系の中でも最も基本となるケースは期待値となるパラメータを全てチェックする
全てのケースで全てのパラメータをチェックするのはやりすぎだと思うが、最低限基本となるケースは全て確認する。
DRYにしすぎない
shared_context
やshared_example
やテスト用の便利モジュールなどを作った場合、
今テストを理解するのに必要な情報が1ファイルに集まらなくなってしまう。
このような場合、初めてテストを読んだ人が色々なファイルを行ったり来たりしてテストを理解せねばならなくなる。
そのテストが何をやっているかを素早く理解するため、可能な限りcontext
内部にテストの入力値、処理内容、期待値が全て集まるように意識している。リクエストパラメータはできるだけケースの直前に毎回べた書きで書いておくとそのテストの入力値が理解しやすい。
1テストケースに1リクエスト
次の同値分割と境界値にも関連するが、1テストケースに1リクエストを意識すると、テストの対応関係がより明確になりやすい。 1テストケースに複数リクエストのテストは、同値分割における同値クラス内の値をテストしている場合が多い。代表値を1つだけテストしても、テストとしての効果は変わらない。
同値分割と境界値を意識する
上記のテストのseatsというパラメータは1〜20
の値を取り、0
以下または21
以上は更新できないとする。
このとき、1〜20
という値はどれでも振る舞いとしては同じになり、同値クラスである。
また、0
や-3
は下限を超えた同値クラス、21
や50
は上限を超えた同値クラスである。
これら同値クラスの中では振る舞いは全て同じになるはずなので、代表値1つだけを抽出してケースを書く。
また、0
,1
,20
,21
は振る舞いの境界値であり、ここもケースを書く。
現実には同値クラスに分類してもケースが多い場合がほとんどなので、実装時間に余裕がある場合は豊富に用意するが、そうでない場合は頻度が高いケースを選定して書く。
まとめ
普段テストを書いているときに考えていることを書いた。 たまにサボってしまい、既存のテストでこのパターンがあるからこっちもこのパターン書いておけばいいか、みたいな気持ちになる。しかし、そうやって人間もコードもダメになってしまうのは良くない。
社内でも人によってテストの書き方について考え方が全く違うので、この記事を叩き台にしてテストについて議論し、みんなで良いテストを意識して書けるようになりたい。