ユニットテスト:その本質、見逃すもの、適切な活用場面

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カバレッジを追加する →