215
246

テストを書く方針と原則の備忘録

Posted at

こんにちは。サーバエンジニアのnsym-mです。普段はGoでバックエンドの開発などをしています。
最近テストに関する書籍や記事などを色々読み漁ったので、現時点での自分のテストについての考え方を備忘録として残しておきます。
今回の話はWebフロントエンドやiOS/Androidなどでも適用できる汎用的な考え方として記載していますが、ベースの文脈はバックエンド開発になりますのでそのつもりで読んでいただけますと幸いです:pray:

なお、本記事では主にGoogle、『単体テストの考え方/使い方』、@t_wadaさんの発表されている考え方(いわゆる古典学派)に倣っています。

用語整理

よく使われるテストスコープ

  • 単体テスト(ユニットテスト)
    • 人によって定義に差がある
  • 統合テスト(インテグレーションテスト)
  • 結合テスト(E2Eテスト)

単体テストの定義がブレることから、スコープではなく実行時間で判断するテストサイズという考え方をGoogleが提唱しています。

  • スモール
  • ミディアム
  • ラージ

75c825b486ad-20240602.png
ref: https://testing.googleblog.com/2010/12/test-sizes.html

image3-2.png
ref: https://levtech.jp/media/article/column/detail_496/#ttl_4

本記事では単体テスト=スモール、統合テスト=ミディアムに相当するものとして記載します(E2Eについては言及しません)。
『Googleのソフトウェアエンジニアリング』によると単体テストは全体の80%、統合テスト+E2Eテストで20%になるのが良い割合とされています。

単体テストで重視したいこと

  • リファクタリング耐性
  • スコープ対象内の信頼性
  • テスト自体の保守性
  • 実行容易性

リファクタリング耐性

  • 出力値ベースもしくは状態ベース(関数実行後の状態を取得して検証)でテストする
    • 関数内の詳細の実装を検証するテストは書かない
    • ブラックボックステストでテストする
  • 関数の「実装」にではなく「振る舞い」に対してテストする
    • その関数が「何をする機能として振る舞うか」という目的を達成できているかテストする
      • (=出力値ベースもしくは状態ベースのテスト)
  • テストはパブリック関数に書く
    • プライベート関数に書くと実装の詳細に依存した脆いテストになる
  • モックは出来るだけ使用しない
    • 実装内部で呼び出す関数に依存しテストが脆くなる
      • モックにはテストを書きやすくするというメリットもあるが、脆いテストが増えてしまうのはむしろ負債となるので避ける方が良い
    • DBをモックするとクエリ側に含まれたビジネスロジックを検証できず偽陰性(テストでのバグの見落とし)が発生しうる
      • クエリからビジネスロジックを完全に取り除くのは難しい
    • モックを使用するケース
      • アプリケーションの管理下にないプロセス外依存
        • 外部サービスの提供するWeb API
        • フロントエンド/アプリから見たWeb API
      • DBはバックエンド目線でいえば管理下にあるプロセス外依存と言えるので基本モックにすべきでない
        • DBが複数アプリケーションとの共通利用などであれば管理下にない場合もある
    • 使ってもいいかもと悩むケース
      • DBを呼び出すUseCaseの役割をする関数が複雑でその関数単体でも(クエリの検証にはならないとしても)ロジックのテストを書きたい場合
        • 先にリファクタリングしてロジックをモデルに移すべき

スコープ対象内の信頼性

  • 異常系テストをたくさん書く
    • 統合テストでは異常系を書きづらいので単体テストで出来るだけカバーする
  • 偽陽性を防ぐ
    • テスト対象にバグはないのにテストが失敗してしまうこと
      • DBカラム変更で関係ない既存のテストが失敗した、など
    • 偽陽性のテスト失敗が多いとテスト結果が信頼されなくなり重要なバグを見逃す
    • リファクタリング耐性の項目で記載した内容を実施することで防げる
  • テスト対象スコープが限られていることから偽陰性の発生を防ぐのは難しい
    • より上位の信頼性担保は統合テストの役割

テスト自体の保守性

  • テストコードにロジックを含めない
  • テストの保守性が悪いとどうなるか
    • 機能改修時に認知負荷が上がり修正に時間がかかるようになる
  • 保守性の理想は「機能要件が変わらない限り2度と変更の必要がないテスト」
  • 失敗時のエラーメッセージを明確にする
    • どこに問題があり、何を直せばいいのかメッセージだけでわかる
  • テストする挙動に合わせて命名する
    • 日本人のチームであれば関数名は日本語の方が良いかも

実行容易性

  • ローカルでもコマンド1つで簡単に実行できる
  • プッシュ時、PR作成時などにCIで自動実行される

統合テストで重視したいこと

  • リファクタリング耐性
  • 単体テストでカバーできない範囲の信頼性
  • 保守性(出来るだけ)

リファクタリング耐性

  • インターフェイスを保っていれば内部は自由にリファクタリングが可能になる
  • 単体テストよりもリファクタリング耐性が大きく高まる

単体テストでカバーできない範囲の信頼性

  • フレームワークが実行するミドルウェア
  • DBやクエリの実行
  • 最終的なレスポンスの形式
  • データの忠実性を
  • テストを通して1つのDBが使用されるため1つのAPIに対して複数のテストケースの実行ができない場合もある
    • テストケース実行ごとに対象のデータをリセットするか、その分のテストは単体テストで補うかが必要
  • 統合テストでも「設定」の変更をテストできない場合も多い
    • 設定ミスでバグが発生するというケースもそれなりにある点は留意が必要

保守性(出来るだけ)

  • テスト実行環境、テストデータを用意などで単体テストよりも仕組みが複雑になるので保守性が下がる
  • テストケースが増えるほどデータ用意のコードが増え保守に苦労する
    • 担当プロダクトではここに改善の余地がある
  • 統合テストの性質上、既存のものを変更するよりも今ある仕組みの上に新機能のテストを追加することが多い
    • 追加しやすくなるよう整える必要がありそう

その他

  • DBはモックせず統合テストで実行する、とはいえクエリの単体テストをできるようにしたい気持ちもある
  • Usecaseが複雑になってしまった部分にどうにかテストを追加したいので、ここはやはりDBまでモックするのがいいんだろうな、という気持ちもある
  • 統合テストの保守性については思案中なので良い案絶賛募集中です

参考文献

215
246
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
215
246