/note/tech

変更につよいユニットテストの書き方

要約:

■ 1. テーマと目標

  • ユニットテストの目的を理解し、プロダクションコードとテストコードの依存関係を作らずに、プログラムの振る舞いに対して各レイヤーの粒度にあった必要十分なテストを書く

■ 2. 前提知識: ユースケースクラス

  • ユースケースクラスの定義:
    • APIのリクエスト・レスポンス以外のプログラムが何をするのかを書くクラス
    • ADRパターンのACTIONクラスと同等の概念として捉えてよい
  • ADRパターンの構成:
    • Action: リクエストを受け取り、ドメインの処理結果をレスポンダに渡す
    • Domain: 必要な処理を行い結果を返すビジネスロジックを含む
    • Responder: ドメインの処理結果を受け取り、必要な準備を行いレスポンスを返す

■ 3. ユニットテストの目的

  • よくある誤解:
    • ユニットテストはプログラミングの品質を保証し、バグがないようにテストするものである
  • 正しい定義:
    • ユニットテストは開発者が想定している通りにプログラムが振る舞うことをテストを書きながら検証・保証するもの(デベロッパーテスト)
    • 開発者が仕様を誤解していたり仕様自体に誤りがある場合はバグが発生する
    • 品質保証はE2Eテストや受け入れテストが担う
  • メンテナンスしやすいユニットテストの条件:
    • プロダクションコードが変更してもテストコードが壊れない
    • 開発者が実装した振る舞いを行うことが保証されている最小限のテストコード
    • メンテナンスしにくいユニットテストは開発者の足かせになる

■ 4. プロダクションコードとテストコードの依存関係をつくらない

  • 依存関係の定義:
    • プロダクションコードの定義をテストコードでも参照するため、プロダクションコードの変更時にテストコードも壊れる状態
  • 依存が生まれる例:
    • メソッド名変更時にテストコードも修正が必要になる(例: User::fetchNewest()User::fetchAuthenticatedNewest()
    • private Methodに対してテストコードを書く(private MethodはPublic Methodより変更頻度が高い)
  • 依存の弊害:
    • CIを実行するたびに Error: Call to undefined method などのエラーが頻発する
    • 結果としてテストを書きたくなくなる
  • 対策: プログラムの振る舞いに対してテストを書く

■ 5. プログラムの振る舞い

  • 定義:
    • 定義された仕様に従って動作すること
    • 仕様の重要な部分はユースケースとして記述される
  • ユースケース記述の構成要素:
    • アクター: 誰が何をするか
    • 前提条件: ユースケースを実行できるための条件
    • メインフロー: ユースケースとして何をしていくか
    • 代替フロー: メインフローが実行できない場合にどうするか
    • 事後条件: ユースケースの実行後に達成される結果や状態
  • テスト方針:
    • ユースケース記述どおりに振る舞っていることをテストする(正常系・異常系)
    • 異常系テストはカスタム例外クラスの発生を確認する(例外メッセージ文字列には依存しない)
    • 複雑なロジック(例: ブラックリスト判定条件)はクラス単位のテストを別途書く
    • シンプルなロジック(例: 外部APIへのアドレス確認)はユースケーステストだけで十分

■ 6. 各レイヤーの粒度にあった必要十分なテストを書いていく

  • テスト構成(ユースケースクラスがある場合):
    • Featureテスト: APIが正しく振る舞っていることの確認
    • UseCaseテスト: ユースケース記述どおりに振る舞っているかの確認
    • 個別クラスのテスト: 実装者が複雑と判断したクラスに対してのみ
  • Featureテストの範囲:
    • リクエストパラメーターのバリデーションが正しく実行されること
    • ユースケースが実行されていること
    • 正しいレスポンスコードが返されること
    • UseCaseクラスのクラス名・メソッド名には依存しない(UseCaseテストで振る舞いが保証されているため)
  • UseCaseテストの範囲:
    • ユースケース記述どおりに正常・異常の振る舞いをしているかを確認する
  • クラスに対するテストの方針:
    • 実装者が自信を持てないもの、複雑と判断するものに対してのみ書く
    • 一般的に簡単でも実装者が難しいと思う場合はクラスに対するテストを書く
    • テストコードが少ないほどメンテナンスするものが少なくなり、変更に強いユニットテストになる

■ 7. 新しい仕様追加時のテスト方針

  • 在庫チェック(RDBで在庫管理、在庫なしの場合はエラーステータスコードを返す):
    • UseCaseテスト: 在庫がない場合にエラーが発生することをテスト
    • Featureテスト: エラーステータスコードが返されることをテスト
    • 在庫チェックはDBにデータがあるかどうかのシンプルなロジックのためクラス単体テストは不要
  • ポイント付与(会員ステータス・商品種類・期間・初回購入・高額購入など複雑なルール):
    • ポイント計算クラスを作成し、そのクラスに対するテストを書く
    • UseCaseテスト: 何かしらのポイントが付与されていることを確認するだけでよい(詳細はクラステストで保証)
    • Featureテスト: APIの振る舞いに変化がないため追加しない