メインコンテンツへスキップ
友田 陽大
フロントエンド
Next.js
フロントエンド
アクセシビリティ
TypeScript
アーキテクチャ設計
パフォーマンス

Playwright E2E テスト設計ガイド【2026年版】— 壊れない・速い・信頼できるテストを本番品質で

Playwright で本番品質の E2E テストを設計する完全ガイド。ロールベースのロケーターと Web-first アサーション(自動待機)でフレークを排除し、外部 API をモックし、a11y を CI で検査し、本番ビルドに対して実行する構成、トレース/レポートによる可観測性、CI シャーディングまで、実コードで解説します。

公開日
読了時間
10分
著者
友田 陽大
シェア

E2E は「リリースして大丈夫か」を最後に保証する砦です。だからこそ、遅くて壊れやすいテストは無いほうがマシ。設計が品質を決めます。


1. 何を E2E で・何を単体で検証するか

テストには階層があります。すべてを重い E2E で書くと、遅く・脆く・高コストになります。

対象適したテスト理由
純粋ロジック(バリデーション・計算)単体(Vitest 等)最速・最安・網羅しやすい
ユーザー動線(入力→送信→確認)E2E(Playwright)実ブラウザでの統合を保証
SEO 配管(JSON-LD・sitemap・RSS)E2Eレンダリング後の出力を検証できる
アクセシビリティの自動検査E2E(axe)実 DOM に対して走らせる

原則は「価値の高い数本の E2E に集中」。例えばこのサイトでは、フォーム送信・ナビゲーション・予約導線・a11y を E2E で押さえ、データ整形やレート制限の窓計算は単体テストに寄せています。


2. ロケーター:ロール基準で書く(壊れにくさ=a11y)

最も重要な設計判断がロケーター戦略です。page.locator(".btn-primary") のような CSS セレクタは、デザイン変更で即壊れます。**ユーザーが認識する手がかり(役割・ラベル・テキスト)**で要素を掴みます。

// ❌ 実装詳細に依存。クラス名が変わると壊れる
await page.locator("div.form > button.submit").click();

// ✅ ユーザー視点。役割とアクセシブルネームで掴む
await page.getByRole("button", { name: "送信する" }).click();
await page.getByLabel("メールアドレス").fill("taro@example.com");
await page.getByRole("radio", { name: /プロジェクト単位/ }).click();

副次効果が強力です。getByRole / getByLabel で要素を掴めるということは、その要素にアクセシブルな名前が付いている証拠です。ロケーター戦略がそのまま a11y のスモークテストになります(WCAG 2.2 実装ガイド)。


3. Web-first アサーション:固定スリープを捨てる

Playwright のアサーションは自動で待機・再試行します。「表示されるまで」「有効になるまで」を Playwright が面倒見るので、waitForTimeout は基本不要です。固定スリープはフレークの最大要因です。

// ❌ フレークの温床:環境が遅いと落ち、速いと無駄に待つ
await page.click("button");
await page.waitForTimeout(3000);
expect(await page.isVisible(".success")).toBe(true);

// ✅ 条件が満たされるまで自動で待つ(タイムアウトまで再試行)
await page.getByRole("button", { name: "送信する" }).click();
await expect(page.getByText("お問い合わせを送信しました")).toBeVisible();

例外:意図的な時間ゲート(例「送信は表示から2.5秒後まで無効」というスパム対策)を検証する場合に限り、その時間を待つのは正当です。「不確実さを誤魔化すためのスリープ」と「仕様としての待機」は区別します。


4. 外部 API のモック:本物に到達させない

E2E でメール送信・決済・課金などの外部副作用を本物に飛ばしてはいけません(コスト・信頼性・セキュリティの三重の問題)。page.route でリクエストを傍受し、ペイロードの形を検証しつつ、任意のレスポンスを返します。

import { test, expect, type Page, type Route } from "@playwright/test";

// /api/contact を傍受し、Resend 等の本物に到達させない。
// 送信ボディの形を確認しつつ、指定のステータス/JSON を返す。
async function mockContactApi(
  page: Page,
  { status = 200, body = { success: true }, onRequest }: {
    status?: number;
    body?: Record<string, unknown>;
    onRequest?: (payload: unknown) => void;
  } = {},
) {
  await page.route("**/api/contact", async (route: Route) => {
    onRequest?.(route.request().postDataJSON());
    await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body) });
  });
}

これで「正常系」「429(レート制限)」「500(サーバーエラー)」を確定的に再現できます。回復性(失敗時の UI)も、外部を待たずに検証できます。

test("レート制限時の文言を出す", async ({ page }) => {
  await page.goto("/contact");
  await mockContactApi(page, { status: 429, body: { error: "rate limited" } });
  // ...入力して送信...
  await expect(page.getByText("短時間に複数回送信されています")).toBeVisible();
});

送信ボディの形を onRequest で捕まえ、toMatchObject で検証すると、「正しいペイロードで・1回だけ」到達したことまで保証できます。


5. アクセシビリティを E2E に組み込む

@axe-core/playwright で、実 DOM に対して WCAG 違反を自動検査します。デザイン変更でコントラストやラベルが壊れても CI で気づけます。

import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

test("主要ページに重大な a11y 違反がない", async ({ page }) => {
  await page.goto("/");
  const results = await new AxeBuilder({ page })
    .withTags(["wcag2a", "wcag2aa", "wcag22aa"])
    .analyze();
  expect(results.violations).toEqual([]);
});

自動検査は万能ではありません(検出できるのは機械判定可能な3〜5割)。キーボード操作やスクリーンリーダーの体感は手動確認が要ります。それでも「退行を CI で止める」価値は絶大です。


6. SEO 配管のテスト:レンダリング後の出力を検証する

構造化データ(JSON-LD)・sitemap・RSS・robots は、壊れても画面では気づきにくい。E2E ならレンダリング後の出力を直接検証できます。

test("ブログ記事に BlogPosting 構造化データが出る", async ({ page }) => {
  await page.goto("/blog/tanstack-query");
  const raw = await page
    .locator('script[type="application/ld+json"]')
    .first()
    .textContent();
  const data = JSON.parse(raw ?? "{}");
  expect(data["@type"]).toBe("BlogPosting");
  expect(data.headline).toBeTruthy();
});

集客に直結する SEO 要素を「壊れたら落ちる」状態にしておくと、リリースのたびに安心できます。


7. 本番ビルドに対して実行する構成

next dev には開発専用の挙動(未最適化・追加ログ)があります。E2E は**本番ビルド(next start)**に対して走らせ、本番に近い条件で検証します。Playwright の webServer がサーバー起動を管理します。

// playwright.config.ts(要点)
import { defineConfig, devices } from "@playwright/test";
const PORT = Number(process.env.PLAYWRIGHT_PORT ?? 3100);

export default defineConfig({
  testDir: "./tests/e2e",
  expect: { timeout: 7_500 },
  fullyParallel: true,
  forbidOnly: Boolean(process.env.CI),     // CI で .only を禁止
  retries: process.env.CI ? 2 : 0,         // 再試行は CI のみ
  use: {
    baseURL: `http://127.0.0.1:${PORT}`,
    trace: "on-first-retry",               // 失敗を再現する証拠を残す
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    { name: "chromium-desktop", use: { ...devices["Desktop Chrome"] } },
    { name: "mobile-safari", use: { ...devices["iPhone 14"] } }, // モバイルも検証
  ],
  webServer: {
    command: `npx next start --port ${PORT}`, // 専用ポートで本番ビルドを起動
    url: `http://127.0.0.1:${PORT}`,
    reuseExistingServer: !process.env.CI,     // ローカルは再利用、CI は毎回新規
    timeout: 120_000,
  },
});

専用ポート(例 3100)にすると、別端末で next dev を動かしたままでも衝突しません。デスクトップとモバイルの両プロジェクトで、レスポンシブの破綻も拾えます。


8. 可観測性とフレーク対策(信頼性)

E2E が「たまに落ちる」状態は、テスト全体の信頼を失わせます。落ちないテストにする設計が要です。

  • トレース・スクショ・動画を失敗時に残す。 trace: "on-first-retry" で、再現困難な失敗も Trace Viewer で時系列に追えます。
  • 再試行は CI のみ・少回数。 ローカルで retries: 0 にすると、フレークを開発中に発見できます。CI は 1〜2 回。
  • テスト間を独立に。 共有状態に依存しない。各テストは自分でセットアップする(beforeEachgoto)。
  • 並列実行を前提に設計する。 fullyParallel でも壊れないよう、テスト固有のデータを使う。
  • 固定スリープを使わない(第3章)。

9. CI での実行:速さとコストの最適化

  • シャーディングで分割並列。 --shard=1/4 のようにジョブを分け、ブロブレポートを後でマージすると、総時間を短縮できます。
  • 段階的に走らせる。 PR では主要ブラウザ+重要動線、main へのマージ時に全ブラウザ+全シナリオ、と段階化するとコスト効率が上がります。
  • ブラウザバイナリをキャッシュ。 npx playwright install の取得をキャッシュして CI 時間を削減。
  • HTML レポートを成果物に。 失敗時の調査を速くします。

「全部を毎回・全ブラウザで」は YAGNI になりがちです。守りたいリスクに対して、実行範囲を設計します。


10. アンチパターン

  • waitForTimeout で待つ。 フレークの主因。Web-first アサーションで自動待機する。
  • CSS/XPath セレクタに依存する。 デザイン変更で壊れる。ロール/ラベルで掴む。
  • 本物の外部 API(決済・メール)に到達させる。 コスト・信頼性・セキュリティの問題。page.route でモック。
  • next dev に対してテストする。 本番ビルド(next start)で検証する。
  • テスト間で状態を共有する。 順序依存はフレークと調査困難を生む。各テストを独立に。
  • 何でも E2E で書く。 純粋ロジックは単体へ。E2E は価値の高い動線に集中。
  • 実装詳細(内部 state・関数)をテストする。 ユーザーに見える振る舞いを検証する。

11. FAQ(よくある質問)

Q. E2E はどれくらいの数を書くべき? A. 「壊れたら致命的な動線」に絞るのが基本です。テストトロフィーの考え方では、土台を単体・結合で厚くし、E2E は少数精鋭にします。

Q. getByRolegetByTestId、どちらを使う? A. まず getByRole / getByLabel(ユーザー視点=a11y にも効く)。それで一意に掴めない場合の最後の手段として data-testid を使います。

Q. ログインが必要なフローはどうする? A. 認証状態を storageState に保存して使い回すのが定石です。毎テストでログイン UI を踏まず、高速かつ安定します。

Q. フレーク(不安定テスト)が止まらない。 A. 固定スリープ・順序依存・共有状態・実装詳細依存のいずれかが大半の原因です。Web-first アサーションと独立性で潰し、Trace Viewer で根本原因を特定します。

Q. Playwright と Cypress の違いは? A. Playwright はマルチブラウザ(Chromium/WebKit/Firefox)対応、並列実行、自動待機、トレースが強力です。本番ビルド検証や CI 並列との相性も良好です。


まとめ:E2E は「リリースの信頼」を作る設計物

E2E テストは、書けば安心というものではありません。壊れにくく・速く・確定的であって初めて、チームが信頼してリリースできます。

  1. 役割分担。 純粋ロジックは単体、動線・SEO・a11y は E2E。
  2. ロール基準のロケーターで、壊れにくさと a11y を両取りする。
  3. Web-first アサーションで固定スリープを排除し、フレークを断つ。
  4. 外部 API はモックし、コスト・信頼性・セキュリティを守る。
  5. 本番ビルドに対して実行し、可観測性(トレース)と CI 並列で運用に耐えさせる。

テストが信頼できると、開発速度が上がります。「変更が怖くない」状態こそ、保守性と拡張性の本質です。

本番運用に耐える E2E・テスト基盤の構築、あるいは品質保証の仕組みづくりが必要な場合は、お気軽にご相談ください。 下記の事例では、複数サービスを束ねる社内プラットフォームを、品質と信頼性を重視して設計・実装した過程を紹介しています。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

国内大手放送事業者の番組制作を支援する社内AIプラットフォーム(マルチサービス基盤・認証ハブを構築)

ケーススタディを見る