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

この記事はトレタ 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は振る舞いの境界値であり、ここもケースを書く。

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

まとめ

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

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

参考文献

エンジニアの立ち居振舞い:話しかけられやすい雰囲気を出す

お題「エンジニア立ち居振舞い」

エンジニアは集中しているとき話しかけられると困る、のような記事がインターネットには多い。(実際集中が途切れると困るのだが)

しかし、そういった記事を読んだ他職種の人達は逆に話しかけづらくなってしまい、 エンジニアに話しかけるタイミングが難しいと思ってしまう人も多いんじゃないかと思う。

自分はできるだけ、話しかけやすい雰囲気を意識して出すということを心がけている。 例えば、

  • 仕事中はできるだけイヤホンをしない
  • 誰かに話しかける時は常に笑顔で明るいトーンで話す
  • 朝早く出社することが多いため、オフィスの入り口近くで仕事をしながら出社して来る人全員に「おはようございます」と言う
  • どんなに小さなことでも誰かに何かをしてもらったときはお礼をする
  • 会社の飲み会などの場合、エンジニア以外の普段話さない人の席の隣に座る

など。

話しかけられやすいと誰かに思ってもらうためには、まずは自分から話しかけにいき、自分の人柄を知ってもらわなければならない。

良いプロダクトはエンジニアだけでは決して作れず、多様な職種の人の力が結集して生まれる。そこにコミュニケーションは必須であり、円滑なコミュニケーションができるように日々努力しなければいけないと思う。

逆に集中したくて話しかけられると困る時は、パーカーのフードを被って「集中してます」アピールをする。

ISUCON5予選に参加した #isucon

ふらわーおんざへっど☆ (@m_nakamura145,@gomachan_7,@grubrescue)というチームでISUCON5予選に参加してきました。全員が今回ISUCON初出場。ヒカリエのLINEオフィスで作業してました。(オフィスめっちゃ綺麗だった)

予選前はせっかくなのでみんなあまり使わないGoでやるぞ!ってノリでしたが、本番でGoの実装にバグがあると連絡が来て自信がなくなり全員安定して書けるrubyに変更しました。

やったこと

役割分担としてはインフラ周りを@grubrescue、コードとSQL周りを@m_nakamura145、@gomachan_7の2人でという感じでした。

最初の一時間は全員でレギュレーション&コードを読み込み。その後クエリを書き換えていましたがうまく書き換えられなかったので @grubrescueにバトンタッチ。僕はunicornとnginxのworker数を調整してました。@gomachan_7はmy.cnfの設定をしてもらってました。 しかし、色々設定を変えてもとにかくベンチマークがFAILしまくって悩んでました。CSSが読み込まれない、POSTがタイムアウトするなどのエラーが出てしまい、スコア外の部分でチーム全員で時間を使ってしまったのが非常に痛かったです。 各自自分のインスタンスを立てて修正、提出用インスタンスにマージしようというやり方でしたが、そのマージもうまくいかず、 逆にスコアが下がってしまってみんなで首を捻ってました。

最終スコアは残念でしたが、チームメンバー全員でISUCONを楽しめたし、とにかくパフォーマンスチューニングの知識不足という感じでもっと勉強しようという気持ちが高まったので良い経験になりました。本戦出場したチームのメンバーに聞いても、構成はそのままで地道にクエリの書き換えと適切なインデックスを張るで本戦出場のスコアに達したらしいので基礎力が本当に大事なんだなと痛感しました。

来年も開催されて欲しいです!!ぜひ参加します!!

GCPが最高に使いやすかったので今後も使っていきたいです。