E2Eテスト:役立つケースとパイプラインを遅くするだけのケース

ユニットテストはすべての関数をチェックしている。結合テストはデータベースクエリを検証している。契約テストはAPIの取り決めが守られていることを確認している。パイプラインはグリーンだ。すべてがうまくいっているように見える。

ところがデプロイ後、ユーザーから「ログインできて、商品を検索できて、カートにも追加できるが、チェックアウトボタンを押しても何も起こらない」と報告が来る。エラーもない。クラッシュもない。ただ沈黙があるだけだ。

個々のコンポーネントは単独で正しく動いていた。しかし一緒になると失敗した。

これこそが、エンドツーエンドテスト(E2Eテスト)が埋めようとするギャップだ。しかし、そのギャップを埋めるにはコストが伴い、注意しなければパイプラインの速度とチームの士気を損なう可能性がある。

E2Eテストが実際に行うこと

E2Eテストは、実際のユーザーと同じようにシステム全体を実行する。アプリケーションが起動し、データベースには本番さながらのデータが入り、外部APIも実際に呼び出される。ブラウザやモバイルクライアントが実際の操作を行う。

完全な購入フローを考えてみよう。ログイン、商品検索、カート追加、支払い情報入力、注文確定、レシート表示。何もモックしない。何もテストダブルで置き換えない。システムのすべてのピースが参加する。

E2Eテストがパスしたときの確信は本物だ。重要なフローが通れば、ユーザーが主要機能で行き詰まることはないと、かなりの確信を持てる。しかし、その確信には代償が伴う。

E2Eテストの本当のコスト

1つのE2Eテストに数分かかることもある。それを数十のシナリオに掛け合わせれば、パイプラインの時間は数時間になる。しかもそれは順調にいった場合の話だ。

E2Eテストは、コードとは無関係な理由で失敗する。テスト環境が遅くてタイムアウトする。前回の実行から残ったテストデータが不整合を起こす。並列テストでデータベースのコネクションプールが枯渇する。外部APIがテストリクエストをレート制限にかける。

テストがランダムに失敗し始めると、チームはパイプラインを信用しなくなる。開発者は調査もせずに「再実行」ボタンをクリックし始める。テストスイートはシグナルではなくノイズと化す。

だからこそ、E2Eテストをむやみに適用してはいけない。何をテストし、どのように実行するかを、外科的に選択する必要がある。

E2Eテストが本当に必要なケース

E2Eテストが必要になるのは、そのシナリオを他の種類のテストでは検証できない場合だ。これは通常、単一のユーザージャーニーの中で複数のコンポーネントを横断するフローを指す。

決済フローは古典的な例だ。ユニットテストで金額計算ロジックを検証できる。結合テストで注文がデータベースに保存されることを確認できる。契約テストでペイメントゲートウェイへのリクエスト形式が正しいことを確認できる。しかし、実際のユーザーが購入を最初から最後まで完了できることを証明できるのは、E2Eテストだけだ。

もう一つの基準はリスクだ。ある機能が壊れたときに大きな損害をもたらすなら、E2Eテストに値する。ログインは良い候補だ。なぜなら、それが壊れると誰もアプリケーションを使えなくなるからだ。チェックアウトも収益に直接影響するため同様だ。

一方、めったに変更されないユーザープロフィールページや、社内の3人しか使わない管理機能は、結合テストでカバーできるだろう。リスクは低く、E2Eテストのコストに見合わない。

全員を待たせずにE2Eテストを実行する方法

本当にE2Eカバレッジが必要なシナリオを特定したら、次の課題は、開発者を永遠に待たせることなくパイプラインでそれらを実行することだ。

重要なユーザージャーニーに限定する。 アプリケーションがユーザーに価値を提供するために必須のフローを特定する。Eコマースサイトなら、検索、商品詳細、カート追加、チェックアウト、決済だろう。メッセージングアプリなら、ログイン、メッセージ送信、メッセージ受信、ログアウトだろう。それ以外は、より高速で安価な方法でテストする。

テストを並列実行する。 各3分かかるテストが10個あるとする。直列に実行すればパイプラインに30分追加される。並列実行すれば、同時実行環境を支える十分なインフラがあれば、3分に短縮できる。これは投資する価値がある。

E2Eテストをパイプラインの独立したステージに分離する。 ユニットテストや結合テストと混ぜてはいけない。高速なテストを先に実行し、開発者に素早いフィードバックを与える。それらがすべてパスした後にのみ、低速なE2Eテストを実行する。こうすることで、開発者はE2Eスイートを待つことなく、数分以内にユニットテストの失敗を知ることができる。

コミットごとにサブセットを実行し、完全スイートは定期的に実行する。 すべてのコード変更に対してすべてのE2Eテストを実行する必要はない。最も重要なものだけを各コミットで実行する。完全なセットは夜間または本番リリース前に実行する。これにより、速度とカバレッジのバランスが取れる。

以下は、E2Eテストを夜間スケジュールまたは手動トリガーのみで実行し、メインのコミットパイプラインを高速に保つYAMLパイプライン設定の例である。

# azure-pipelines-e2e.yml
# E2Eテスト用の独立したパイプライン

trigger: none  # コミットごとには実行しない

schedules:
- cron: '0 2 * * *'  # 毎日午前2時に実行
  displayName: Nightly end-to-end tests
  branches:
    include:
    - main

resources:
  pipelines:
  - pipeline: mainBuild
    source: main-ci
    trigger:
      branches:
        include:
        - main

jobs:
- job: e2e_tests
  displayName: 'Run End-to-End Tests'
  pool:
    vmImage: 'ubuntu-latest'
  steps:
  - script: |
      echo "Starting end-to-end test suite..."
      npm run test:e2e
    displayName: 'Execute E2E tests'

テストを環境の不安定さに耐性のあるものにする。 タイムアウトによる失敗にはリトライを使用する。各テストの実行前にテストデータを既知の状態にリセットする。技術的な理由でテストが頻繁に失敗するなら、テストか環境を修正する。信頼性の低いテストにパイプラインへの信頼を損なわせてはいけない。

実践的なチェックリスト

新しいE2Eテストを追加する前に、以下の質問を自問する。

  • このシナリオはより高速なテスト種別で検証できないか?
  • このフローが壊れた場合、影響を受けるユーザー数とその深刻度は?
  • これは重要なユーザージャーニーか、それともあると便利なフローか?
  • このテストを他のテストと並列実行できるか?
  • テストは環境問題に対して耐性があるか?

最初の質問が「はい」なら、E2Eテストを書くな。リスクが低いなら、書くな。重要なジャーニーでないなら、書くな。

まとめ

E2Eテストは、システム全体が正しく動作するという最高レベルの確信を与える。しかし、その確信には時間、インフラ、メンテナンスという大きなコストが伴う。最も重要なフローにのみ使用し、チームがパイプラインを嫌うボトルネックにならないよう賢く実行すること。

信頼性高く実行される、厳選された少数のE2Eテストは、誰も信用しない100の不安定なテストよりも価値がある。