LiBz Tech Blog

LiBの開発者ブログ

普通のチームで立ち向かう - Railsアプリでのテストコードポリシー -

皆さん自分たちのプロダクトにテストコードは書いているでしょうか?

LiBzCAREERではRspecやCIなどテストコードを書くための仕組みは割と初期から用意していました。 ただし、テストコードを書くべきかの基準が明確になっておらず、テストコードが書かれているかは実装者に任されていました。

それではテストコードの書く書かないの判断が、実装者のテストコードに対するスキルや志向によってバラけてしまうため、今回の開発チーム再編のタイミングでチーム全員でテストコードを書くという意思決定をし、テストコードポリシーを定めました。

チームやプロダクトの状況

自分が所属する開発チームの体制はこちら

  • エンジニア5人
  • Ruby on Railsを採用
  • チームにこれまでもテストコードを書いてきたエンジニアは1名だけ
  • 既存のプロダクトコードにもテストがない箇所が多々ある
  • SPAやJSフレームワークの導入など、フロントエンドで複雑なコードは少ない

というのがチームやプロダクトの状況です。

テストコードポリシー

テスト戦略

なぜテストを書くのか

テストを書くのは後々の変化に強くなるためです。

例外もありますが基本的には「新規開発でテストコードを書いていると開発スピードが落ちる」のは事実でしょう。ただし、私達が作っているプロダクトは必ず変化します。それもコードを全く新しく追加するよりも、コードを変更することの方が多いでしょう。よってテストコードを書くことによって「初期開発コストは高くなるが、全体コストが下がって楽になる」ということを目指します。

逆説的に「ここのコードは変更されることはないからテストコードを書くのは割に合わない」という場面もあるでしょう。

どこにテストを書くべきか

全てのコードに対してテストが書かれているのが理想ではありますが現実的ではありません。 そこでオブジェクトの種類によって優先度を決めました。

Railsのアプリケーションでは具体的には以下のような箇所は優先度が高いでしょう。

優先度 高

  • Modelクラス
  • Serviceクラス
  • Validatorクラス

これらにはビジネスロジックが凝縮されている事が多いので優先度を高くしています。 特にServiceクラスは複数のモデルを組み合わせるような複雑なロジック

優先度 低

  • Controllerクラス

コントローラは基本的にテストコードを書くコストが高くなりがち。

  • 1アクションのテストするだけでも必要なパラメータを用意して、アクセスさせて、様々なインスタンス変数の中身を確認する
  • パラメータも様々なパラメータを一度に送るアクションなどでの組合せ爆発

ControllerのテストよりもControllerから呼んでいるModelやServiceクラスのpublicメソッドをテストして担保するのが望ましい。

逆にModelにテストを書いてもテストに不安が残るようなら、コントローラのプロダクトコードにビジネスロジックが漏れ出している。設計を見直して、コントローラから適切なクラスにロジックを移譲すること。

コントローラでは「HTTP 200 OKを返すこと」という単純な正常系のテストを1つだけ書いておくというのは一つの手です。 これではロジックのバグまでは検出できませんが、syntaxエラーやRailsアップデートで非推奨になったメソッド、削除されたメソッドを発見するのに役立ちます。

  • Viewクラス

    • テストコードで担保すべきロジックが少ない
    • デザイン変更に伴ってテストコードを書き直すことになると変更コストが大きいため優先度を下げている
  • Railsの設定だけで済む部分

    • モデルのシンプルな関連(belongs_toやhas_many)やバリデーション
    • 独自実装のバリデーションメソッド、凝った正規表現、条件付きのアソシエーションなどはテストコードを書く

その他

他にもこういった要素があるコードはテストコードを書く優先度が高くなるでしょう。

  • 更新頻度が高い
  • テストケースが多くて手動での網羅テストが難しい
  • 課金や個人情報管理などのバグが起こったときの被害が大きい
  • バグを発生させたことがある箇所

テスト戦術

テストコードを書くときに意識すべきこと

テスト1件ごとの見通しを良くする

テスト項目が多い、1件のテストコードが多いときにメソッドや変数を多くすると見通しが悪くなります。

「テストコードで使用されている変数を追っていたらずーとスクロールしたファイル先頭で let 定義されていた」など、テスト仕様を理解するのにスクロール距離が長いような場合は注意。 shared_examplesなどを使用する場合も別ファイルを見に行かなくてはいけないので、特にこのデメリットに留意する

テストコードにロジックを入れない

テストコードにloop処理, if分岐が入っていたら要注意。 ストコードにロジックが入り込むと「テストコードのバグ」を作り込む要因にもなります。他のエンジニアが見たときにも対象のプロダクトコードのロジックがどんな仕様なのか理解しにくくなり、そうするとテストコードの正当性をテストするといった無限ループに突入してしまいます。

ユースケースはif分岐ではなくcontextから分けてロジックを排除しましょう。 DRYではなくなりますが、ロジックが明確になりテストコードから仕様も理解しやすくなります。

サンプル

Bad

describe '#even?' do
  [1, 2].each do |object|
    context '偶数だったら' do
      it 'treuを返す' do
        if object == 2
          expect(object.even?).to be true
        else
          expect(object.even?).to be false
        end
      end
    end
  end
end

Good

describe '#even?' do
  context '偶数だったら' do
    let(:object) { 2 }
    it 'treuを返す' do
      expect(object.even?).to be true
    end
  end

  context '奇数だったら' do
    let(:object) { 1 }
    it 'falseを返す' do
      expect(object.even?).to be false
    end
  end
end

分かりやすい説明を書く

テストコードには describe, context, it に適切な説明を書きましょう。

リブでは説明は日本語でおkにしています。過去に書かれたテストコードにはは英語で書かれている部分が多いですが、暫く運用してみた感想として

  • 無理して英語で書くことで必要な情報の漏れる
  • 誤った情報の記載が発生する
  • あとから読んだときのテスト仕様把握のハードルが高い

という問題がありました。

また、テスト実行結果を見やすくするためにも、describe, context, itへの説明は適切に使い分けましょう

  • describe: テスト対象、クラス名、メソッド名、ユースケース
  • context: 文脈、条件
  • it: 期待値

というように使い分けています。

サンプル

Bad

describe '#in_contract_term?` do
  context '契約期間内の企業はin_contract_term?がtrueを返すこと' do
    it do
      expect(compay.in_contract_term?).to be_true
    end
  end
end

Good

describe '#in_contract_term?` do
  context '企業が契約期間内だったら' do
    it 'trueを返すこと' do
      expect(compay.in_contract_term?).to be_true
    end
  end
end

itは expect(Object.update_primary_flg).to be_true のように検証内容が ひと目見て分かるくらいシンプルなものなら、説明は省略してもよい

テストコードを書きやすい設計

テストコードを書くには、プロダクトコードがテストしやすい設計になっていることも大切です。 テストコードが書きにくいと感じたら、プロダクトコードの方をまずはリファクタリングしてみるのも手です。

※そもそも既存コードをリファクタリングするためにもテストコードがないと悩ましいですが、、、

テストコードが書きにくいのはこういったオブジェクトです

  • 責務が多い
  • 引数が多い
  • 副作用がある

こういうオブジェクトにテストコードを書こうとすると、一つの対象に対してテストケースが大量になってしまいます。

また、これらの要素はSOLID原則に反するものです。 テストしにくいと感じたら、それはプロダクトコードが変更しづらいコードになっていると言えるかもしれません。

今後の課題

このポリシーでテストコードを書くべきコードの判断基準にある程度の共通認識ができたと思います。 ただし、ポリシーを決めてもテストコードのような工程を増やすことものがすぐに全員に浸透させるのは難しいでしょう。

今後は

  • ポリシーに違反してテストコードが書かれていないコードに対してはレビューで指摘する
  • テストコードを書く工数を確保した見積もり&スケジューリング
  • テストコードの書き方などについてチームで議論しながらポリシーをアップデートしていく

などの活動で地道に習慣化させて、文化として根付かせたいと思います。