テストを書くときに気をつけていること

この記事はトレタ 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_contextshared_exampleやテスト用の便利モジュールなどを作った場合、 今テストを理解するのに必要な情報が1ファイルに集まらなくなってしまう。 このような場合、初めてテストを読んだ人が色々なファイルを行ったり来たりしてテストを理解せねばならなくなる。

そのテストが何をやっているかを素早く理解するため、可能な限りcontext内部にテストの入力値、処理内容、期待値が全て集まるように意識している。リクエストパラメータはできるだけケースの直前に毎回べた書きで書いておくとそのテストの入力値が理解しやすい。

1テストケースに1リクエスト

次の同値分割と境界値にも関連するが、1テストケースに1リクエストを意識すると、テストの対応関係がより明確になりやすい。 1テストケースに複数リクエストのテストは、同値分割における同値クラス内の値をテストしている場合が多い。代表値を1つだけテストしても、テストとしての効果は変わらない。

同値分割と境界値を意識する

上記のテストのseatsというパラメータは1〜20の値を取り、0以下または21以上は更新できないとする。 このとき、1〜20という値はどれでも振る舞いとしては同じになり、同値クラスである。 また、0-3は下限を超えた同値クラス2150は上限を超えた同値クラスである。

これら同値クラスの中では振る舞いは全て同じになるはずなので、代表値1つだけを抽出してケースを書く。 また、0,1,20,21は振る舞いの境界値であり、ここもケースを書く。

現実には同値クラスに分類してもケースが多い場合がほとんどなので、実装時間に余裕がある場合は豊富に用意するが、そうでない場合は頻度が高いケースを選定して書く。

まとめ

普段テストを書いているときに考えていることを書いた。 たまにサボってしまい、既存のテストでこのパターンがあるからこっちもこのパターン書いておけばいいか、みたいな気持ちになる。しかし、そうやって人間もコードもダメになってしまうのは良くない。

社内でも人によってテストの書き方について考え方が全く違うので、この記事を叩き台にしてテストについて議論し、みんなで良いテストを意識して書けるようになりたい。

参考文献