E2E(End-to-End)テストは、ユーザーと同じ経路でアプリケーション全体を動かし、主要な業務フローが壊れていないことを保証するためのテストです。カバー範囲が広いぶん、遅く壊れやすくなりがちなので、「何を・どう書くか」を意識的に設計することが重要です。

目次

  1. 戦略・スコープ
  2. テスト設計
  3. セレクタ(要素の特定)
  4. 待機とフレーキー対策
  5. テストデータ管理
  6. 認証の扱い
  7. 外部依存の扱い
  8. コードの構造化
  9. アサーション
  10. 環境とCI
  11. クロスブラウザ・デバイス
  12. 運用・メンテナンス
  13. ツール選定の目安

1. 戦略・スコープ

テストピラミッドを守る

  • E2Eは少数精鋭にする。ロジックの網羅はユニットテスト、モジュール間の結合は統合テストに任せる。
  • 目安の比率: ユニット 70% / 統合 20% / E2E 10%(チームの状況で調整)。
  • 「E2Eでしか検証できないこと」だけをE2Eで書く。

クリティカルパスを優先する

優先度の高い順に対象を選ぶ。

  1. 売上・業務に直結するフロー(購入、決済、申込み など)
  2. 認証・認可(ログイン、ログアウト、権限による表示の出し分け)
  3. 障害時の影響が大きいフロー(データ登録・更新・削除)
  4. 過去に本番障害が起きた箇所(回帰防止)

バリデーションの全パターンをE2Eで網羅しない

入力値の境界値や細かなエラーメッセージはユニット/コンポーネントテストで確認し、E2Eでは「エラー時にエラーが表示される」代表ケースを1つ程度にとどめる。


2. テスト設計

ユーザー視点のシナリオで書く

  • 「ボタンAをクリック → 画面Bに遷移」ではなく、「ユーザーが商品をカートに入れて購入できる」という振る舞い単位で書く。
  • テスト名はシナリオがそのまま読める文にする。
test('ログイン済みユーザーは商品をカートに追加して購入を完了できる', async ({ page }) => {
  // ...
});

1テスト = 1つの目的

  • 1つのテストに複数のシナリオを詰め込まない。失敗時に原因を特定しにくくなる。
  • ただし、細かく分けすぎて前提操作の繰り返しで遅くなるなら、関連するアサーションをまとめるのは可。

テストは独立させる

  • テスト間で状態(データ、ログイン状態、実行順序)に依存しない。
  • どのテストを単体で実行しても、ランダム順で実行しても通ること。
  • 並列実行を前提に設計する。

AAA(Arrange-Act-Assert)で構造化する

test('商品を検索できる', async ({ page }) => {
  // Arrange: 前提状態の準備
  await seedProducts([{ name: 'りんご' }, { name: 'みかん' }]);
  await page.goto('/products');
 
  // Act: ユーザー操作
  await page.getByRole('searchbox', { name: '商品検索' }).fill('りんご');
  await page.getByRole('button', { name: '検索' }).click();
 
  // Assert: 結果の検証
  await expect(page.getByRole('listitem')).toHaveCount(1);
  await expect(page.getByRole('listitem')).toContainText('りんご');
});

3. セレクタ(要素の特定)

壊れにくさに最も影響するポイント。実装の詳細ではなく、ユーザーに見えるもので要素を探す。

推奨順位

  1. ロール + アクセシブルネーム(getByRole('button', { name: '送信' }))
  2. ラベル(getByLabel('メールアドレス'))
  3. プレースホルダー・テキスト(getByPlaceholder / getByText)
  4. テスト専用属性(data-testid="submit-button")
  5. CSSクラス、XPath、DOM構造に依存したセレクタ → 避ける

ポイント

  • ロールベースのセレクタはアクセシビリティの検証も兼ねられる。
  • data-testid は「ユーザーに見えるもので特定しづらい要素」の逃げ道として使う。
  • .btn-primary > span:nth-child(2) のようなセレクタはデザイン変更で即座に壊れる。
  • 文言が頻繁に変わる・多言語対応している場合は、テキスト依存を減らし data-testid を検討する。

4. 待機とフレーキー対策

フレーキー(不安定)なテストは信頼を失わせ、最終的に誰も結果を見なくなる。最優先で潰す。

固定時間の待機を使わない

// ❌ NG: 環境によって足りなかったり、無駄に遅かったりする
await page.waitForTimeout(3000);
 
// ✅ OK: 条件が満たされるまで自動で待つ
await expect(page.getByText('保存しました')).toBeVisible();

自動待機(auto-wait)を活かす

  • Playwright や Cypress のアサーション・操作は、要素が操作可能になるまで自動でリトライする。これを前提に書く。
  • 「要素を取得 → 値を取り出して → 自前で比較」すると自動リトライが効かない。Web-first なアサーション(expect(locator).toHaveText() 等)を使う。

ネットワーク・非同期処理を明示的に待つ

  • 特定のAPIレスポンスを待つ必要がある場合は、レスポンス待機を明示する。
const responsePromise = page.waitForResponse('**/api/orders');
await page.getByRole('button', { name: '注文する' }).click();
await responsePromise;

フレーキーの主な原因と対策

原因対策
固定待機・タイミング依存条件ベースの待機に置き換える
テスト間のデータ共有テストごとにデータを作成・分離する
外部サービスの不安定さモック/スタブ化する
アニメーションテスト環境でアニメーションを無効化する
日時・乱数時計を固定する、シードを固定する
実行順序への依存独立性を確保し、ランダム順で検証する

リトライは「保険」であって「解決策」ではない

  • CIでのリトライ(1〜2回)は許容するが、リトライで通ったテストはフレーキーとして記録・追跡する。
  • フレーキーなテストは隔離(quarantine)して修正チケットを切る。放置しない。

5. テストデータ管理

テストごとにデータを用意する

  • 共有のテストデータ(「テスト用ユーザーA」など)を複数テストで使い回さない。並列実行時に競合する。
  • 一意な値(タイムスタンプやUUIDを含むメールアドレスなど)でデータを作る。

UIではなくAPI・DBでセットアップする

  • 前提データの作成をUI操作で行うと遅く、壊れやすい。
  • APIやDBへの直接投入、ファクトリ関数でセットアップし、検証したい操作だけをUIで行う。

後片付けを確実に

  • テスト後にデータを削除する、またはテストごとに使い捨ての環境・スキーマを使う。
  • 「前回のテストのゴミ」で失敗する状況を作らない。

6. 認証の扱い

  • ログインフロー自体のテストは1〜数本に限定する。
  • それ以外のテストでは、ログイン済みの状態(Cookie / ストレージ)を保存して再利用する。
// Playwright の例: セットアップで一度ログインして状態を保存
// playwright.config.ts の projects で storageState を指定して再利用する
await page.context().storageState({ path: 'playwright/.auth/user.json' });
  • ロールごと(管理者・一般ユーザーなど)に認証状態を用意しておくと権限テストが書きやすい。

7. 外部依存の扱い

モックする / しないの判断基準

対象方針
自社のバックエンド・DB原則本物を使う(E2Eの意義)
決済・メール送信・SMS などの外部サービスサンドボックス環境またはモック
外部API(天気、地図など)モック(レスポンスを固定)
障害・エラー時の挙動モックで意図的にエラーを返す
  • モックしすぎると「E2Eなのに本番と違う経路を通っている」状態になる。何をモックしているか明確にしておく。
  • 外部サービスとの実際の結合は、契約テスト(Contract Test)や別途の監視で補完する。

8. コードの構造化

Page Object Model(POM)で操作をまとめる

画面ごとの要素と操作をクラスにまとめ、テスト本体からセレクタを隠す。UI変更時の修正箇所が1か所で済む。

// pages/LoginPage.ts
export class LoginPage {
  constructor(private page: Page) {}
 
  async goto() {
    await this.page.goto('/login');
  }
 
  async login(email: string, password: string) {
    await this.page.getByLabel('メールアドレス').fill(email);
    await this.page.getByLabel('パスワード').fill(password);
    await this.page.getByRole('button', { name: 'ログイン' }).click();
  }
}
// tests/login.spec.ts
test('正しい認証情報でログインできる', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password');
  await expect(page.getByRole('heading', { name: 'ダッシュボード' })).toBeVisible();
});

POM運用の注意点

  • アサーションはPage Objectに入れず、テスト側に書く(何を検証しているかをテストから読めるようにする)。
  • Page Objectを巨大化させない。画面内のコンポーネント単位で分割してもよい。
  • 過度な抽象化は避ける。テストが「読んでわかる」ことを優先する。

共通処理はフィクスチャ/ヘルパーに

  • セットアップ・後片付けはフレームワークのフィクスチャ機能(Playwright の fixtures、Cypress の custom commands 等)に寄せる。

9. アサーション

  • ユーザーに見える結果を検証する(表示テキスト、遷移先URL、要素の表示/非表示)。
  • 必要に応じて、UI操作の結果をAPI・DBで裏取りする(「保存しました」の表示だけでなく実際に保存されたか)。
  • 1テスト内のアサーションは「そのシナリオの成功条件」に絞る。無関係な要素まで検証しない。
  • スナップショット/ビジュアル比較は差分ノイズが出やすい。対象を絞り、閾値やマスク(日時・動的要素)を設定する。

10. 環境とCI

環境

  • 本番に近い構成の専用環境で実行する(ステージング、またはコンテナで都度起動する環境)。
  • 環境ごとの差分(URL、認証情報)は環境変数で切り替える。認証情報はシークレット管理する。
  • 可能ならDockerなどでローカルでもCIと同じ環境を再現できるようにする。

CIでの実行

  • 並列実行・シャーディングで実行時間を短縮する。
  • 実行タイミングを分ける:
    • PR時: スモークテスト(クリティカルパスのみ、数分以内)
    • main マージ後・夜間: フルスイート
    • デプロイ後: 本番/ステージングでのスモークテスト
  • ヘッドレスで実行し、失敗時のみ詳細な成果物を残す。

失敗時のデバッグ情報を残す

  • スクリーンショット
  • 動画
  • トレース(Playwright Trace Viewer など)
  • コンソールログ・ネットワークログ

CIで失敗したテストを「手元で再現できない」状態をなくすことが重要。


11. クロスブラウザ・デバイス

  • 対象ブラウザは利用者のアクセス解析に基づいて決める。全ブラウザで全テストを回す必要はない。
  • 例: Chromium でフルスイート、Firefox / WebKit ではスモークのみ。
  • モバイル対応が重要なら、ビューポートやデバイスエミュレーションでのテストを追加する。

12. 運用・メンテナンス

  • テストも本番コードと同じ品質基準でレビューする。
  • 失敗したテストを放置しない。「赤いのが普通」になった時点でE2Eは機能しなくなる。
  • 実行時間・成功率・フレーキー率を定期的に計測し、悪化したら改善に時間を割く。
  • 価値の低いテスト(重複している、ユニットテストで代替できる)は削除する勇気を持つ。
  • 本番障害が起きたら、再発防止のE2E(またはより下層のテスト)を追加する。

13. ツール選定の目安

ツール特徴
Playwrightマルチブラウザ(Chromium / Firefox / WebKit)、自動待機、並列実行、トレース機能が充実。新規導入の第一候補になりやすい
Cypress開発者体験が良く、テスト実行の可視化が強力。ブラウザ内で動作するため一部制約あり
Selenium / WebDriver歴史が長く、多言語・多環境に対応。既存資産がある場合に有力
Appium / Detox / MaestroモバイルアプリのE2E

選定時は「チームの言語」「対象ブラウザ・プラットフォーム」「CIとの相性」「デバッグのしやすさ」で比較する。


チェックリスト

テストを書いたら確認する。

  • クリティカルなユーザーシナリオを対象にしているか
  • ユニット/統合テストで代替できる内容ではないか
  • テスト名からシナリオが読み取れるか
  • 他のテストに依存せず、単独・並列・ランダム順で通るか
  • セレクタはロール/ラベル/テキスト/data-testid を使っているか
  • 固定時間の待機(sleep / waitForTimeout)を使っていないか
  • 前提データはAPI/DBで用意し、後片付けしているか
  • ログインはUIを毎回通さず、保存した認証状態を再利用しているか
  • 外部サービスは適切にモック/サンドボックス化されているか
  • アサーションはユーザーに見える結果を検証しているか
  • 失敗時にスクリーンショット・トレースが残るか
  • ローカルで繰り返し実行して安定して通るか

0件のページ