ユニットテスト:その役割、見落とすもの、そして活用すべき場面

Yunhao Jiao
ユニットテスト:その役割、見落とすもの、そして活用すべき場面 カバー

ユニットテストは自動テストの中で最も広く実践されている形式であり、同時に最も誤解されやすいものでもあります。ほとんどのテスト戦略の基盤となる層ですが、唯一のテスト手法として使用した場合に、チームに誤った安心感を与えやすい層でもあります。

このガイドでは、ユニットテストが実際に何を行うのか、何を行わないのか、そして完全なテスト戦略の一層として効果的に活用する方法について説明します。

ユニットテストとは何か?

ユニットテストとは、コードの単一の独立したユニット(通常は関数またはクラスメソッド)を、その依存関係から完全に分離した状態で検証する自動テストです。

この「分離」がユニットテストの本質的な特徴です。ユニットテストではモック、スタブ、またはフェイクを使用して、すべての外部依存関係(データベース、API、ファイルシステム、他のモジュール)を置き換え、テスト対象ユニットのロジックのみを検証します。

ユニットテストが答える問いは「この関数は、この特定の入力を受け取ったとき、この特定の出力を返すか?」というものです。その関数が呼び出し元から正しく呼ばれているか、処理するデータが現実的かどうか、またはシステム全体がエンドツーエンドで動作するかどうかは検証しません。

ユニットテストが得意とすること

純粋なビジネスロジック。税計算、価格ルール、割引ロジック、日付演算、文字列操作など、副作用のない入出力関数。ユニットテストはここで最適なツールです:高速で、決定論的で、メンテナンスが容易です。

複雑な分岐ロジック。多くのコードパス(多数のif条件、エラーケース、エッジケース)を持つ関数は、各パスを検証するユニットテストから恩恵を受けます。ユニットテストの速度と分離性により、すべての分岐をテストすることが現実的になります。

ユーティリティ関数とライブラリ。コードベース全体で使用される共通関数には、徹底的なユニットテストカバレッジが必要です。ユーティリティ関数のバグは多くの利用箇所に影響を及ぼす可能性があり、ユニットレベルで検出することは、E2E失敗として検出するよりもはるかにコストが低いです。

既知バグのリグレッション防止。バグが修正された際に、そのバグを検出できたはずのユニットテストを追加することで、再発を防ぎます。これはソフトウェア開発において最も信頼性の高い品質向上の取り組みの一つです。

ユニットテストにできないこと

統合ポイントの検証ができない

ユニットテストはすべての依存関係をモック化します。そのため、以下の検証ができません:

  • コードがデータベースを正しく呼び出し、正しい結果を取得できるか
  • あなたのAPIリクエストのフォーマットが実際のサーバーの期待する形式と一致していること
  • あなたのサードパーティ統合が実際のAPIのレスポンススキーマを正しく処理していること
  • あなたの認証チェックが実際のリクエストコンテキストにおいて未承認のリクエストを確実にブロックしていること

モック化された依存関係はすべて信頼の主張です。「この依存関係はこのように動作すると信じている」というものです。ユニットテストは、その主張が正しいと仮定した上でコードが機能することを検証します。統合テストおよびE2Eテストは、その主張自体を検証します。

意図のギャップを検出できない

これは、AIコーディングツールを使用するチームにとって最も重要な制限です。ユニットテストは一般的に、実装を書いたのと同じシステムによって(または書いた後に)作成されます。実装が間違っている場合—もっともらしい動作をしているが実際の要件と一致しない場合—ユニットテストはたいてい誤った実装を正しいと確認してしまいます。

例として考えてみましょう。AIコーディングエージェントが割引計算関数を実装します。関数は内部的に正しく、計算も合っています。しかし、要件では税後合計に対して割引を計算するよう指定されていたにもかかわらず、税前合計に対して割引を計算していました。この関数に対して書かれたユニットテストは計算を検証し、パスします。しかし、要件は満たされていません。

このクラスのバグを検出できるのは、実装からではなく仕様から導出された、要件ベースのテストだけです。TestSpriteの仕様駆動型エージェントテストは、まさにこの目的のために設計されています。

ユーザーエクスペリエンスの問題を検出できない

ユニットテストはコードレベルで動作し、ユーザーレベルでは動作しません。500件のユニットテストがすべてパスしていても、ユーザーが実際にサインアップフローを完了できるか、チェックアウトボタンがモバイルで表示されているか、ローディング状態が正しくレンダリングされているかについては何も教えてくれません。

創発的な振る舞いを検出できない

複数のコンポーネントが連携したときにのみ現れるバグがあります。2つのサービス間のタイミングバグ、並行リクエスト処理におけるレースコンディション、1つの依存関係が遅延したときのカスケード障害—これらは創発的な振る舞いであり、(分離してテストする)ユニットテストでは構造上検出できません。

ユニットテストのベストプラクティス

実装ではなく振る舞いをテストする。テストは関数が何をするかを検証すべきであり、どのようにするかを検証すべきではありません。内部実装の詳細を検証するテストは、振る舞いが変わっていなくてもリファクタリングで壊れます。内部実装が書き直されても有効であり続けるテストを書きましょう。

1テストにつき1アサーション(原則として)。複数の振る舞いを同時に検証するテストは、失敗したときに診断が難しくなります。各テストは、真でなければならない1つのことを明確に表現すべきです。

テストには説明的な名前をつける。テスト名は、何を検証するかを説明する文にすべきです。例えば、`test_discount_1`ではなく`calculates_discount_as_percentage_of_post_tax_total`のようにします。

エッジケースを明示的にテストする。ハッピーパスだけをテストしないでください。各関数について、空の入力、null/undefinedの入力、最小・最大の境界値、エラー条件を明示的にテストしましょう。

ユニットテストを高速に保つ。ユニットテストはミリ秒単位で実行されるべきです。秒単位かかるテストには、モック化すべき依存関係があります。テストスイートが遅いと、チームはスキップするようになります。

完全な戦略の中でのユニットテスト

ユニットテストは必要ですが、それだけでは十分ではありません。完全なテスト戦略では以下を組み合わせて使用します:

  • ロジックの正確性のためのユニットテスト(実装に対する迅速なフィードバック)
  • 契約と境界の正確性のための統合テスト
  • ユーザーフローと要件の正確性のためのE2Eテスト — TestSpriteがこれを自律的に処理します

よくあるアンチパターンは、ユニットテストのカバレッジが高くE2Eカバレッジがないチームが、よくテストされた関数が組み合わさって壊れたユーザーエクスペリエンスを生み出していることに気づくというものです。個々の関数はすべて動作しているのに、プロダクトとしては機能していないのです。

ユニットテストはパーツが正しいことを教えてくれます。E2Eテストはプロダクトが正しいことを教えてくれます。両方が必要です。

ユニットテストスイートに要件ベースのE2Eカバレッジを追加する →