パイプラインの各ステージでテストをどこに配置すべきか
コードをプッシュして待つ。5分が経過。10分。まだパイプラインは動いている。ようやく失敗したと思ったら、最初の30秒で実行すべきだったユニットテストのエラーだった。あなたは10分の時間とコンピューティングリソースを無駄にしたことになる。
これがパイプライン設計における核心的なトレードオフです。早い段階でテストをやりすぎると、開発者はフィードバックを待つ時間が長くなります。少なすぎると、重大な障害がステージングや本番環境にすり抜けてしまいます。解決策は、すべてをどこでも実行することではなく、各テストタイプを最も価値が高くコストが低いステージに配置することです。
原則:まずは高速かつ低コストで
ルールは単純です。高速で低コストなテストを早期に実行し、低速で高コストなテストは後で、かつ前段のテストが通過した後にのみ実行します。
ここで「低コスト」とは、計算時間、環境セットアップ、データ準備が少ないことを意味します。ミリ秒で実行されデータベースを必要としないユニットテストは低コストです。フル環境を起動し、データをシードし、ユーザージャーニーをシミュレートするエンドツーエンドテストは高コストです。
「高速」とはフィードバックループを指します。開発者は変更が基本的な部分を壊したかどうかを1〜2分以内に知る必要があります。30分待って基本的なロジックエラーを発見するのは許容できません。
これはポリシーの問題ではありません。スループットの問題です。開発者が早くフィードバックを得られれば得られるほど、問題を早く修正できます。テストの実行コストが高ければ高いほど、実行回数は少なくすべきです。
ステージごとのテスト配置
コミットステージ:ユニットテストとコントラクトテスト
次のフローチャートは、パイプラインステージ全体での推奨テスト配置をまとめたものです。
開発者がコードをプッシュしたら、最初に実行するのはユニットテストです。これらのテストは高速で、外部依存関係を必要とせず、個々の振る舞いが正しく動作することを検証します。優れたユニットテストは、呼び出し元やユーザーの観点から意味のある振る舞いが期待通りの結果を生成することを証明します。内部実装の詳細には関心を持ちません。
一部のチームはこのステージでコントラクトテストも実行します。変更がインターフェースやAPIコントラクトに影響を与える場合、早期に検証することで下流での驚きを防げます。コントラクトテストは通常、ユニットテストと一緒に実行できるほど高速です。
ここで実行すべきでないもの:結合テスト、エンドツーエンドテスト、データベースや外部サービス、フル環境を必要とするテスト。これらは後で実行します。
ビルドステージ:結合テストとコントラクトテスト(再)
コードがコンパイルされ、アーティファクトまたはコンテナイメージが生成された後、結合テストが登場します。このステージでは、アプリケーションは実行可能なユニットとして存在します。結合テストは、アプリケーションがデータベース、メッセージキュー、その他のサービスなどの依存関係に正しく接続できることを確認します。
これらのテストは通常、テストダブルや軽量コンテナを使用し、フルステージング環境は使いません。配線が正しいこと、つまりアプリケーションが接続を開き、クエリを送信し、応答を処理できることを検証します。
コントラクトテストは、ビルドされたアーティファクトに対してここでも実行されることがよくあります。これにより、コンパイルされたコードがソースコードだけでなく、コントラクトを満たしていることを確認します。
ステージングステージ:エンドツーエンドテストとスモークテスト
ステージングは可能な限り本番環境をミラーリングする必要があります。ここでエンドツーエンドテストを実行しますが、重要なユーザージャーニーに限定します。すべてのページ、すべての機能ではなく、最も重要なフロー(ログイン、チェックアウト、検索、またはコアビジネスロジック)だけです。
エンドツーエンドテストは低速で高コストです。軽微な変更ごとに実行するのは時間の無駄です。クリティカルパスに触れる変更や新機能を導入する変更のために予約しておきます。
スモークテストもここで実行します。スモークテストは、デプロイ後にアプリケーションが正しく応答するかを迅速に確認するものです。ステージングでスモークテストが失敗した場合は、本番に進まず、パイプラインを停止して調査します。
本番ステージ:スモークテストとシンセティックトランザクション
本番環境では、テストは最小限に抑え、ユーザーに影響を与えてはいけません。デプロイ直後にスモークテストを実行し、主要なエンドポイントが正しいステータスコードを返すことを確認します。これは深い検証ではなく、デプロイによってアプリケーションが壊れていないことを確認する健全性チェックです。
シンセティックトランザクションは、すべてのデプロイ後ではなく、定期的に実行します。ログインや購入完了などのユーザー操作をシミュレートし、ステージングで捕捉できなかったリグレッションを検出します。スケジュールに基づいて実行されるため、メモリリークやデータ破損など、時間の経過とともに現れる問題を捕捉します。
やってはいけないこと
すべてのテストをすべてのステージで実行してはいけません。これが最も一般的な間違いです。コミットステージでエンドツーエンドテストを実行するのは意味がありません。環境が準備できておらず、フィードバックが遅すぎます。本番ステージでユニットテストを再実行するのは無駄です。ステージングと本番の間でロジックは変わっていません。
良い経験則:テストが前のステージで合格した場合、環境の変更が結果に影響を与える可能性がある場合を除き、後のステージで繰り返さないこと。ユニットテストは環境に依存しないため、再実行する必要はありません。スモークテストはデプロイに依存するため、新しい環境ごとに実行する必要があります。
パイプラインにおけるリスクベースのテスト
テストの配置はリスクにも依存します。支払いシステムや認証ロジックへの変更は、より多くのテストレイヤーを必要とします。ボタンの色の変更やラベルのタイポは、ユニットテストとスモークテストのみで十分です。
これはパイプラインに適用されたリスクベースのテストです。変更のリスクが高いほど、通過しなければならないテストレイヤーが多くなります。リスクが低いほど、パイプラインをより速く通過できます。
一部のチームは、変更にタグを付けたりブランチポリシーを使用してこれを実装しています。クリティカルパスはステージングでエンドツーエンドテストを必要とします。非クリティカルパスはそれらをスキップします。これにより、低リスクの変更ではパイプラインを高速に保ちながら、高リスクの変更では安全性を維持できます。
テスト配置の実践的チェックリスト
- ユニットテストはコミットステージで実行。高速、依存関係なし、即時フィードバック。
- コントラクトテストはコミットステージとビルドステージで実行。インターフェースを早期に、かつアーティファクトに対して検証。
- 結合テストはビルドステージで実行。コンテナやテストダブルを使用して依存関係への接続を確認。
- エンドツーエンドテストはステージングステージのみで実行。重要なユーザージャーニーをカバーし、すべての機能は対象外。
- スモークテストはステージングと本番ステージで実行。各デプロイ後の迅速な健全性チェック。
- シンセティックトランザクションは本番環境で定期的に実行。時間の経過とともにリグレッションを捕捉。
- 環境の変更が結果に影響を与える場合を除き、ステージ間でテストを繰り返さない。
- リスクに基づいてテストレイヤーを調整。高リスクの変更はより多くのカバレッジを得る。
まとめ
適切に配置されたテストは、必要なときに高速なフィードバックを提供し、コストをかけられるタイミングで徹底的な検証を提供します。目標はすべてをどこでもテストすることではありません。目標は、適切なタイミングで適切な障害を捕捉し、チームが本番環境に到達する前に修正できるようにすることです。