LESSON 30分

E2Eテスト(Playwright)

ストーリー

「ユニットテスト、インテグレーションテストの次は E2Eテストだ。ユーザーの目線でシステム全体をテストする」

松本先輩がブラウザを開いた。

「Playwright は Microsoft が開発しているE2Eテスト フレームワークだ。Chromium、Firefox、WebKit の 全てに対応していて、高速で安定している。 実際に触ってみよう」


Playwright とは

Playwright は、Webアプリケーションのエンドツーエンドテストを自動化するフレームワークです。

セットアップ

bash
# インストール
npm init playwright@latest

# ブラウザのインストール
npx playwright install

設定ファイル

typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  retries: 2,
  use: {
    baseURL: 'http://localhost:3000',
    screenshot: 'only-on-failure',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npm run dev',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
  projects: [
    { name: 'chromium', use: { browserName: 'chromium' } },
    { name: 'firefox', use: { browserName: 'firefox' } },
    { name: 'webkit', use: { browserName: 'webkit' } },
  ],
});

基本的なテストの書き方

ページナビゲーションと要素の操作

typescript
import { test, expect } from '@playwright/test';

test('トップページが表示される', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveTitle('My Application');
  await expect(page.locator('h1')).toHaveText('Welcome');
});

test('ログインフォーム', async ({ page }) => {
  await page.goto('/login');

  // フォーム入力
  await page.fill('[data-testid="email"]', 'user@example.com');
  await page.fill('[data-testid="password"]', 'password123');

  // ボタンクリック
  await page.click('[data-testid="login-button"]');

  // ログイン後のリダイレクトを確認
  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('[data-testid="welcome-message"]'))
    .toHaveText('ようこそ、ユーザーさん');
});

ロケーター(要素の取得方法)

typescript
// data-testid(推奨)
page.locator('[data-testid="submit-button"]');

// テキスト
page.getByText('送信');
page.getByRole('button', { name: '送信' });

// ラベル
page.getByLabel('メールアドレス');
page.getByPlaceholder('例: user@example.com');

// CSS セレクタ
page.locator('.submit-button');
page.locator('#email-input');

実践的なE2Eテストシナリオ

ユーザー登録からログインまで

typescript
test.describe('ユーザー認証フロー', () => {
  test('新規ユーザー登録 → ログイン → ダッシュボード表示', async ({ page }) => {
    // 1. ユーザー登録
    await page.goto('/register');
    await page.fill('[data-testid="name"]', 'テスト太郎');
    await page.fill('[data-testid="email"]', `test-${Date.now()}@example.com`);
    await page.fill('[data-testid="password"]', 'SecurePass123!');
    await page.fill('[data-testid="password-confirm"]', 'SecurePass123!');
    await page.click('[data-testid="register-button"]');

    // 登録完了の確認
    await expect(page.locator('[data-testid="success-message"]'))
      .toBeVisible();

    // 2. ログイン
    await page.goto('/login');
    await page.fill('[data-testid="email"]', `test-${Date.now()}@example.com`);
    await page.fill('[data-testid="password"]', 'SecurePass123!');
    await page.click('[data-testid="login-button"]');

    // 3. ダッシュボード確認
    await expect(page).toHaveURL('/dashboard');
    await expect(page.locator('[data-testid="user-name"]'))
      .toContainText('テスト太郎');
  });
});

商品検索と購入フロー

typescript
test.describe('商品購入フロー', () => {
  test.beforeEach(async ({ page }) => {
    // ログイン済み状態にする
    await page.goto('/login');
    await page.fill('[data-testid="email"]', 'buyer@example.com');
    await page.fill('[data-testid="password"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('商品を検索してカートに追加し購入完了する', async ({ page }) => {
    // 1. 商品検索
    await page.goto('/products');
    await page.fill('[data-testid="search-input"]', 'TypeScript');
    await page.click('[data-testid="search-button"]');

    // 検索結果の確認
    const results = page.locator('[data-testid="product-card"]');
    await expect(results).toHaveCount(3);

    // 2. 商品詳細
    await results.first().click();
    await expect(page.locator('[data-testid="product-title"]'))
      .toBeVisible();

    // 3. カートに追加
    await page.click('[data-testid="add-to-cart"]');
    await expect(page.locator('[data-testid="cart-badge"]'))
      .toHaveText('1');

    // 4. カートに移動
    await page.click('[data-testid="cart-icon"]');
    await expect(page.locator('[data-testid="cart-item"]'))
      .toHaveCount(1);

    // 5. 購入手続き
    await page.click('[data-testid="checkout-button"]');
    await page.fill('[data-testid="address"]', '東京都渋谷区...');
    await page.click('[data-testid="confirm-order"]');

    // 6. 注文完了
    await expect(page.locator('[data-testid="order-complete"]'))
      .toBeVisible();
    await expect(page.locator('[data-testid="order-id"]'))
      .toBeVisible();
  });
});

E2Eテストのベストプラクティス

data-testid を使う

html
<!-- ❌ CSSクラスやIDに依存(UIリファクタリングで壊れる) -->
<button class="btn-primary submit">送信</button>

<!-- ✅ data-testid を使う(UIの変更に強い) -->
<button class="btn-primary submit" data-testid="submit-button">送信</button>

テストの安定性を高める

typescript
// ❌ 固定の待ち時間(フレーキーテストの原因)
await page.waitForTimeout(3000);

// ✅ 要素の出現を待つ(安定)
await expect(page.locator('[data-testid="result"]')).toBeVisible();

// ✅ ネットワークリクエストの完了を待つ
await page.waitForResponse(response =>
  response.url().includes('/api/orders') && response.status() === 200
);

テストの独立性

プラクティス説明
各テストで独自のデータを作成他のテストのデータに依存しない
テスト後にクリーンアップ作成したデータを削除
ユニークな値を使用タイムスタンプをメールに含める等
テスト順序に依存しないどの順序で実行しても通る

まとめ

項目ポイント
Playwright高速・安定したE2Eテストフレームワーク
ロケーターdata-testid の使用を推奨
テストシナリオユーザーの操作フローを忠実に再現
安定性waitForTimeout を避け、要素やレスポンスを待つ

チェックリスト

  • Playwright のセットアップと基本構文を理解した
  • ロケーターの使い分け(data-testid 推奨)を理解した
  • ユーザー操作フローのE2Eテストを書ける
  • フレーキーテストを避ける方法を理解した

次のステップへ

E2Eテストを学んだら、次はテスト自動化とCI統合を学びます。テストをCI/CDパイプラインに組み込んで、自動的に品質を守る仕組みを作りましょう。


推定読了時間: 30分