<CodeLearn/>
テスト レッスン4

E2Eテスト

Playwrightでブラウザを操作してアプリ全体をテストしよう

E2Eテストとは?

E2E(End-to-End)テストは、実際のブラウザを使ってユーザーと同じ操作を自動的に行い、 アプリケーション全体が正しく動作するかを検証します。 フロントエンド、バックエンド、データベースまで含めた統合的なテストです。

メリット

  • ユーザー体験に最も近い
  • 全レイヤーを一度にテスト
  • クリティカルパスの保証

デメリット

  • 実行速度が遅い
  • 壊れやすい(Flaky Test)
  • デバッグが難しい

いつ使う?

  • ログインフロー
  • 購入・決済フロー
  • 重要なユーザー導線

Playwright のセットアップ

Playwrightは Microsoft が開発したE2Eテストフレームワークです。 Chromium、Firefox、WebKitの3つのブラウザエンジンに対応しています。

# 初期セットアップ(対話形式で設定)
npm init playwright@latest

# または手動インストール
npm install -D @playwright/test
npx playwright install

# テストの実行
npx playwright test

# UIモードで実行(デバッグに便利)
npx playwright test --ui

# 特定のファイルだけ実行
npx playwright test tests/login.spec.ts

# ブラウザを表示して実行(ヘッドフルモード)
npx playwright test --headed
// playwright.config.ts - 基本設定
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  // テストのタイムアウト(30秒)
  timeout: 30000,
  // 各テストのリトライ回数
  retries: process.env.CI ? 2 : 0,
  // テストレポートの形式
  reporter: "html",

  use: {
    // テスト対象のURL
    baseURL: "http://localhost:3000",
    // 失敗時にスクリーンショットを撮る
    screenshot: "only-on-failure",
    // 操作のトレースを記録
    trace: "on-first-retry",
  },

  // 複数ブラウザでテスト
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
    // モバイルブラウザ
    { name: "mobile", use: { ...devices["iPhone 14"] } },
  ],

  // テスト前にサーバーを起動
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

E2Eテストの書き方

Playwrightでは page オブジェクトを通して ブラウザを操作します。主要なメソッドを見ていきましょう。

// tests/home.spec.ts
import { test, expect } from "@playwright/test";

test.describe("ホームページ", () => {
  test("タイトルが表示される", async ({ page }) => {
    // ページに移動
    await page.goto("/");

    // タイトルを確認
    await expect(page).toHaveTitle(/My App/);

    // 見出しが表示されていることを確認
    await expect(
      page.getByRole("heading", { name: "ようこそ" })
    ).toBeVisible();
  });

  test("ナビゲーションリンクが機能する", async ({ page }) => {
    await page.goto("/");

    // リンクをクリック
    await page.getByRole("link", { name: "概要" }).click();

    // URLが変わったことを確認
    await expect(page).toHaveURL("/about");

    // ページ内容を確認
    await expect(
      page.getByText("このアプリについて")
    ).toBeVisible();
  });
});

// === 主要なページ操作メソッド ===
test("基本的な操作", async ({ page }) => {
  // ページ遷移
  await page.goto("https://example.com");

  // クリック
  await page.getByRole("button", { name: "送信" }).click();

  // テキスト入力
  await page.getByPlaceholder("メールアドレス").fill("test@example.com");

  // セレクトボックス
  await page.getByLabel("都道府県").selectOption("東京都");

  // チェックボックス
  await page.getByLabel("利用規約に同意").check();

  // キーボード操作
  await page.keyboard.press("Enter");

  // 要素の待機
  await page.waitForSelector(".loading-complete");

  // スクリーンショット
  await page.screenshot({ path: "screenshot.png" });
});

アサーションとロケーター

Playwrightのアサーション(expect)は自動的にリトライしてくれるため、 非同期の表示変更にも対応できます。ロケーターで要素を特定する方法も多彩です。

// === ロケーター(要素の特定方法) ===
// ロールベース(推奨)
page.getByRole("button", { name: "送信" });
page.getByRole("heading", { level: 1 });
page.getByRole("link", { name: "ホーム" });

// テキストベース
page.getByText("こんにちは");
page.getByText("こんにちは", { exact: true }); // 完全一致

// フォーム要素
page.getByLabel("ユーザー名");
page.getByPlaceholder("検索...");

// テストID
page.getByTestId("submit-button");

// CSSセレクター(最終手段)
page.locator(".my-class");
page.locator("#my-id");

// === アサーション ===
// 要素の表示
await expect(page.getByText("成功")).toBeVisible();
await expect(page.getByText("エラー")).not.toBeVisible();

// テキスト内容
await expect(page.getByTestId("count")).toHaveText("5");
await expect(page.getByTestId("count")).toContainText("5");

// 属性
await expect(page.getByRole("button")).toBeEnabled();
await expect(page.getByRole("button")).toBeDisabled();
await expect(page.getByLabel("同意")).toBeChecked();

// URL・タイトル
await expect(page).toHaveURL("/dashboard");
await expect(page).toHaveTitle("ダッシュボード");

// 要素数
await expect(page.getByRole("listitem")).toHaveCount(3);

ユーザーフローのテスト

E2Eテストの真価は、ユーザーが実際に行う操作フロー全体をテストすることにあります。 ログインからタスク作成までの一連の流れをテストしてみましょう。

// tests/todo-flow.spec.ts
import { test, expect } from "@playwright/test";

test.describe("TODOアプリの操作フロー", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/todos");
  });

  test("TODOの作成→完了→削除の一連フロー", async ({ page }) => {
    // ステップ1: TODOを追加
    await page.getByPlaceholder("新しいタスク").fill("Playwrightを学ぶ");
    await page.getByRole("button", { name: "追加" }).click();

    // 追加されたことを確認
    await expect(page.getByText("Playwrightを学ぶ")).toBeVisible();
    await expect(page.getByTestId("todo-count")).toHaveText("1件");

    // ステップ2: もう一つ追加
    await page.getByPlaceholder("新しいタスク").fill("テストを書く");
    await page.getByRole("button", { name: "追加" }).click();
    await expect(page.getByTestId("todo-count")).toHaveText("2件");

    // ステップ3: 1つ目を完了にする
    await page.getByText("Playwrightを学ぶ")
      .locator("..") // 親要素
      .getByRole("checkbox")
      .check();

    // 完了状態を確認(取り消し線など)
    await expect(
      page.getByText("Playwrightを学ぶ")
    ).toHaveCSS("text-decoration-line", "line-through");

    // ステップ4: 完了したTODOを削除
    await page.getByText("Playwrightを学ぶ")
      .locator("..")
      .getByRole("button", { name: "削除" })
      .click();

    // 削除されたことを確認
    await expect(page.getByText("Playwrightを学ぶ")).not.toBeVisible();
    await expect(page.getByTestId("todo-count")).toHaveText("1件");
  });

  test("空のタスクは追加できない", async ({ page }) => {
    // 空のまま追加ボタンを押す
    await page.getByRole("button", { name: "追加" }).click();

    // エラーメッセージが表示される
    await expect(
      page.getByText("タスク名を入力してください")
    ).toBeVisible();
  });
});

スクリーンショットとCI統合

Playwrightはスクリーンショットの比較テスト(Visual Regression Test)や、 CI/CDパイプラインへの組み込みも簡単です。

// === スクリーンショットテスト ===
test("ホームページのスクリーンショット", async ({ page }) => {
  await page.goto("/");

  // ページ全体のスクリーンショットを比較
  await expect(page).toHaveScreenshot("homepage.png");

  // 特定の要素だけスクリーンショット
  await expect(
    page.getByTestId("hero-section")
  ).toHaveScreenshot("hero.png");

  // 許容誤差を設定(ピクセル単位)
  await expect(page).toHaveScreenshot("homepage.png", {
    maxDiffPixels: 100,
  });
});

// === スクリーンショットの保存 ===
test("デバッグ用スクリーンショット", async ({ page }) => {
  await page.goto("/dashboard");

  // 任意のタイミングで保存
  await page.screenshot({
    path: "screenshots/dashboard.png",
    fullPage: true, // ページ全体をキャプチャ
  });
});
# === GitHub Actions での CI 設定 ===
# .github/workflows/e2e.yml

name: E2E Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps

      - name: Build application
        run: npm run build

      - name: Run E2E tests
        run: npx playwright test

      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

まとめ

  • E2Eテストは実際のブラウザで操作し、ユーザー体験に最も近いテストを実現する
  • Playwrightは Chromium / Firefox / WebKit の3エンジンに対応
  • page.goto、click、fill などでブラウザ操作をプログラムで記述する
  • getByRole、getByText でアクセシブルなロケーターを使う
  • expect は自動リトライ付きで、非同期の表示変更にも対応
  • スクリーンショット比較テストでビジュアルの回帰をチェックできる
  • GitHub Actionsなどに組み込んで、プルリクエストごとに自動テストを実行する