統合テスト:コンポーネント間の連携で問題を発見する

あなたは正しく見える関数を書きました。ユニットテストはパスします。ロジックはクリーンです。そしてデプロイすると、アプリケーションがエラーを返し始めます。存在すると思っていたデータベースのカラムは先週リネームされていました。呼び出している外部APIはレスポンス形式を変更していました。依存しているサービスは異なるヘッダーを期待するようになっていました。

これこそが、ユニットテストでは埋められないギャップです。関数は単体では完全に正しくても、別のコンポーネントと通信しようとすると失敗することがあります。統合テストは、まさにこの種の問題をキャッチするために存在します。

統合テストが実際にチェックするもの

統合テストは、2つ以上のコンポーネントが正しく連携して動作することを検証します。コンポーネントは、アプリケーションとデータベース、あなたのサービスと外部API、あるいは同じシステム内の2つの内部サービスかもしれません。

それらがキャッチするバグは、めったにロジックの誤りではありません。それらは想定の不一致に関するものです:

以下のシーケンス図は典型的な不一致を示しています。サービスが日付を文字列として送信する一方、データベースがタイムスタンプを期待し、エラーが発生します。

sequenceDiagram participant Service participant Database Service->>Database: INSERT INTO orders (date) VALUES ('2025-04-01') Database-->>Service: ERROR: column "date" is of type timestamp but expression is of type text Note over Service,Database: 想定の不一致: 文字列 vs. タイムスタンプ
  • あなたのコードは日付を文字列として送信するが、データベースのカラムはタイムスタンプを期待している。
  • あなたのサービスはクエリパラメータでAPIを呼び出すが、APIはそのパラメータをリクエストボディに移動した。
  • あなたのアプリケーションはフィールドが常に存在すると想定しているが、上流のサービスは特定の条件下でのみそれを含める。

これらはコードを凝視しても見つけられないバグです。これらはコンポーネントが実際にデータを交換したときにのみ現れます。

脆弱性の罠

統合テストは遅くて壊れやすいという評判があります。この評判は当然です。関与する実際のコンポーネントが多ければ多いほど、コード以外の理由でテストが失敗する可能性が高くなります。ネットワークタイムアウト。依存するサービスのダウン。以前の実行で破損したテストデータ。

これが繰り返し発生すると、チームはテスト結果を信頼しなくなります。統合テストをスキップしたり、失敗を無視したりし始めます。テストはシグナルではなくノイズになります。

解決策は統合テストを避けることではありません。解決策は、実際の依存関係で何をテストするかを選択的にすることです。

実際の依存関係でテストするものを選ぶ

すべての依存関係が統合テストで実際のものである必要はありません。経験則はシンプルです:シミュレートが難しいもの、または本番で頻繁に問題を引き起こすものに対してのみ、実際の依存関係でテストします。

データベースは通常、実際のインスタンスでテストする価値があります。 クエリの動作、制約、トランザクション、ロックは正確にモックするのが困難です。モックはクエリが構文的に正しいことを教えてくれるかもしれませんが、クエリが同時アクセス下でデッドロックを引き起こすことや、マイグレーションがコードがまだ文字列として扱っているカラムの型を変更したことを教えてくれません。

外部のサードパーティAPIは、通常、パイプライン内で実際のエンドポイントを使ってテストする価値はありません。 典型的なレスポンスを記録するテストダブルを使用してください。ネットワーク問題やAPIレート制限によるフレーキーテストのリスクは、メリットを上回ります。実際の統合は、ステージングまたは本番検証のために取っておいてください。

組織内の内部サービスは中間に位置します。 インターフェースが頻繁に変更され、不一致のコストが高い場合は、実際のインスタンスでテストできます。そうでなければ、コントラクトテストの方が、脆弱性が少なく、より良いシグナルを提供することがよくあります。

判断する実用的な方法:自問してみてください。「この依存関係に問題がある場合、アプリケーションロジックからわかるか、それとも接続の仕方からのみわかるか?」答えが「接続の仕方から」— レスポンス形式、ヘッダー構造、パラメータ順序 — であれば、実際の依存関係を使った統合テストの候補です。答えが「アプリケーションロジックから」であれば、ユニットテストまたはコントラクトテストで十分です。

統合テストを高速で信頼性高く保つ

実際の依存関係で何をテストするかを決めたら、次のプラクティスに従って統合テストを有用に保ちます:

接続のみをテストし、ビジネスロジックはテストしない。 すでにユニットテストでビジネスルールをカバーしているなら、統合テストでそれを繰り返さないでください。データベースクエリの統合テストは、クエリが実際のデータベースに対して正常に実行され、期待される構造を返すことを検証するべきです。そのクエリを使用するビジネスロジックのすべてのエッジケースを検証するべきではありません。

各テストの前に環境をリセットする。 データベースを使用する場合は、分離されたテストデータを作成し、テスト終了後にクリーンアップします。以前のテストによって残された状態に依存するテストは、脆弱でデバッグが困難です。各テスト後にロールバックするデータベーストランザクションを使用するか、テスト実行ごとに新しいテストコンテナを起動します。

統合テストの数を制限する。 すべてのパラメータの組み合わせをテストする必要はありません。1つのハッピーパスといくつかの現実的な障害シナリオをテストします。目標は、接続が機能するという確信であり、可能なすべての入力のカバレッジではありません。

統合テストがパイプラインのどこに位置するか

統合テストは、ユニットテストとエンドツーエンドテストの間に位置します。ユニットテストよりもコストがかかりますが、エンドツーエンドテストよりは高速で焦点が絞られています。

典型的なパイプラインは最初にユニットテストを実行します。それらがパスすれば、パイプラインは統合テストを実行します。統合テストがパスすれば、パイプラインはステージングまたは本番デプロイに進みます。エンドツーエンドテストがある場合は、後で、または別の環境で実行されます。

統合テストの目的はカバレッジ率を達成することではありません。目的は、コードが変更されたときに、コンポーネント間の接続がまだ機能するという確信を与えることです。

実践的なチェックリスト

  • 外部依存関係ごとに、実際のインスタンスでテストするか、テストダブルでテストするか、コントラクトテストに頼るかを決定する。
  • 既知の状態にリセットできる分離された環境で統合テストを実行する。
  • 統合テストを接続動作に焦点を当て、ビジネスロジックには焦点を当てないようにする。
  • 統合テストを1つのハッピーパスといくつかの現実的な障害シナリオに制限する。
  • テスト実行時間を監視する。統合テストがユニットテストより長くかかる場合、おそらくテストが多すぎるか、種類が間違っています。

まとめ

統合テストは、ユニットテストでは答えられない質問に答えます:「これらのコンポーネントは実際に連携して動作するか?」その答えは、デプロイする前に得る価値があります。しかし、統合テストはツールであって目標ではありません。実際の依存関係でテストするものを選択的に選び、テストを高速で分離された状態に保ち、カバレッジ数値を追いかけるのではなく、デプロイへの確信を築くためにそれらを使用してください。