E2E(End-to-End)テストは、ユーザーと同じ経路でアプリケーション全体を動かし、主要な業務フローが壊れていないことを保証するためのテストです。カバー範囲が広いぶん、遅く壊れやすくなりがちなので、「何を・どう書くか」を意識的に設計することが重要です。
目次
- 戦略・スコープ
- テスト設計
- セレクタ(要素の特定)
- 待機とフレーキー対策
- テストデータ管理
- 認証の扱い
- 外部依存の扱い
- コードの構造化
- アサーション
- 環境とCI
- クロスブラウザ・デバイス
- 運用・メンテナンス
- ツール選定の目安
1. 戦略・スコープ
テストピラミッドを守る
- E2Eは少数精鋭にする。ロジックの網羅はユニットテスト、モジュール間の結合は統合テストに任せる。
- 目安の比率: ユニット 70% / 統合 20% / E2E 10%(チームの状況で調整)。
- 「E2Eでしか検証できないこと」だけをE2Eで書く。
クリティカルパスを優先する
優先度の高い順に対象を選ぶ。
- 売上・業務に直結するフロー(購入、決済、申込み など)
- 認証・認可(ログイン、ログアウト、権限による表示の出し分け)
- 障害時の影響が大きいフロー(データ登録・更新・削除)
- 過去に本番障害が起きた箇所(回帰防止)
バリデーションの全パターンを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. セレクタ(要素の特定)
壊れにくさに最も影響するポイント。実装の詳細ではなく、ユーザーに見えるもので要素を探す。
推奨順位
- ロール + アクセシブルネーム(
getByRole('button', { name: '送信' })) - ラベル(
getByLabel('メールアドレス')) - プレースホルダー・テキスト(
getByPlaceholder/getByText) - テスト専用属性(
data-testid="submit-button") 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を毎回通さず、保存した認証状態を再利用しているか
- 外部サービスは適切にモック/サンドボックス化されているか
- アサーションはユーザーに見える結果を検証しているか
- 失敗時にスクリーンショット・トレースが残るか
- ローカルで繰り返し実行して安定して通るか