← ブログ一覧

Playwright で始める E2E テスト実践ガイド

Web アプリの E2E テストを Playwright で実装する手順を解説。テストシナリオ設計・Page Object パターン・CI 統合・並列実行まで、受託開発で即使える実践ガイド。

#テスト#CI/CD#TypeScript#技術解説
Playwright で始める E2E テスト実践ガイド

Playwright で始める E2E テスト実践ガイド

Web アプリケーションの品質保証において、E2E(End-to-End)テストは欠かせない要素です。しかし「導入したいけど、どこから手をつければいいか分からない」「メンテナンスコストが高くて続かない」という声をよく聞きます。

この記事では、Microsoft が開発する E2E テストフレームワーク Playwright を使った実践的なテスト実装方法を解説します。テストシナリオの設計から CI/CD への組み込みまで、受託開発の現場で即使える知見をまとめました。


1. Playwright を選ぶ理由と導入前の判断基準

Playwright の主な特徴

| 項目 | Playwright | Cypress | Selenium | |------|------------|---------|----------| | ブラウザサポート | Chromium, Firefox, WebKit | Chromium, Firefox, Edge | 全主要ブラウザ | | 複数タブ・コンテキスト | ○ | △(実験的) | ○ | | 並列実行 | ○(標準) | ○(有料プラン) | ○(要設定) | | 自動待機 | ○ | ○ | △ | | 学習コスト | 中 | 低 | 高 | | 実行速度 | 高速 | 中速 | 低速 | | TypeScript サポート | ○(ファーストクラス) | ○ | △ |

導入を推奨するケース

  • 複数ブラウザでの動作確認が必要:WebKit(Safari エンジン)も含めてテストしたい
  • 認証フローや複数タブの操作が多い:SaaS のダッシュボードなど
  • CI/CD で高速に実行したい:並列実行が標準で可能
  • TypeScript でテストを書きたい:型安全性を活かしたい

導入を見送るケース

  • テスト対象が単純なランディングページのみ:手動テストで十分
  • IE11 対応が必須:Playwright は非対応(Selenium を検討)
  • 開発チームの経験が浅い:Cypress の方が学習コストが低い場合も

2. セットアップと基本設定

インストール手順

# プロジェクトに Playwright を追加
npm init playwright@latest

# 対話形式で以下を選択
# - TypeScript を使用: Yes
# - テストフォルダ名: tests
# - GitHub Actions ワークフロー追加: Yes
# - ブラウザインストール: Yes

初期設定ファイル(playwright.config.ts)

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  // タイムアウト設定(ms)
  timeout: 30 * 1000,
  expect: {
    timeout: 5000,
  },
  // 失敗時のリトライ回数(CI では有効化推奨)
  retries: process.env.CI ? 2 : 0,
  // 並列実行のワーカー数
  workers: process.env.CI ? 1 : undefined,
  // レポート形式
  reporter: [
    ['html'],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
  use: {
    // ベース URL(環境変数で切り替え)
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    // トレース記録(失敗時のみ)
    trace: 'on-first-retry',
    // スクリーンショット(失敗時のみ)
    screenshot: 'only-on-failure',
    // 動画記録(失敗時のみ)
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    // モバイル対応も簡単に追加可能
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
  ],
  // ローカル開発サーバーを自動起動
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

環境ごとの設定切り替え

# .env.local(ローカル開発)
BASE_URL=http://localhost:3000

# .env.ci(CI 環境)
BASE_URL=https://staging.example.com

3. テストシナリオの設計と優先順位付け

優先度別テスト項目の整理

| 優先度 | テスト対象 | 例 | |--------|------------|----| | P0(必須) | ユーザー登録・ログイン | 新規登録 → メール認証 → ログイン | | P0(必須) | コアビジネスロジック | 商品購入、決済フロー | | P1(推奨) | 主要な CRUD 操作 | 記事作成・編集・削除 | | P1(推奨) | 権限制御 | 管理者 / 一般ユーザーの画面切り替え | | P2(任意) | エラーハンドリング | ネットワークエラー時の表示 | | P2(任意) | レスポンシブ対応 | モバイル画面での動作確認 |

テストケース設計の実務 Tips

✅ 推奨パターン

  • ユーザーストーリーごとにテストを分ける:「ログイン → 記事作成 → 公開」を 1 テストにまとめない
  • テストの独立性を保つ:前のテストの成功/失敗に依存しない
  • データのクリーンアップを行うbeforeEach でテストデータをリセット

❌ 避けるべきパターン

  • UI の細かい文言チェック:変更頻度が高くメンテナンスコスト増
  • アニメーションの完了を待つwaitForTimeout の多用は不安定
  • 複数の機能を 1 テストで検証:失敗箇所の特定が困難

4. Page Object パターンによる保守性の向上

Page Object パターンとは

ページごとに操作を抽象化し、テストコードとページ構造を分離するデザインパターンです。UI 変更時の修正箇所を最小化できます。

実装例:ログインページ

// tests/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('メールアドレス');
    this.passwordInput = page.getByLabel('パスワード');
    this.submitButton = page.getByRole('button', { name: 'ログイン' });
    this.errorMessage = page.getByTestId('error-message');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async getErrorMessage() {
    return await this.errorMessage.textContent();
  }
}

テストコードでの使用例

// tests/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test.describe('ログイン機能', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('正しい認証情報でログインできる', async ({ page }) => {
    await loginPage.login('user@example.com', 'password123');
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('ダッシュボード')).toBeVisible();
  });

  test('誤ったパスワードでエラーが表示される', async () => {
    await loginPage.login('user@example.com', 'wrong-password');
    const error = await loginPage.getErrorMessage();
    expect(error).toContain('認証に失敗しました');
  });

  test('未入力でバリデーションエラーが表示される', async () => {
    await loginPage.submitButton.click();
    await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
  });
});

Page Object パターンのメリット

  • UI 変更時の修正が一箇所で済む:ボタンの文言変更など
  • テストコードの可読性向上loginPage.login() のような直感的な記述
  • 再利用性が高い:複数のテストで同じページオブジェクトを使用

5. 認証状態の管理とテストの高速化

問題:毎回ログインすると遅い

全テストで毎回ログイン操作を行うと、実行時間が大幅に増加します。

解決策:認証状態の再利用

// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';

const authFile = 'playwright/.auth/user.json';

setup('認証状態を保存', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password123');
  
  // ログイン後の URL を確認
  await page.waitForURL('/dashboard');
  
  // 認証状態(Cookie・LocalStorage)を保存
  await page.context().storageState({ path: authFile });
});

各テストで認証状態を読み込む

// playwright.config.ts に追加
export default defineConfig({
  projects: [
    // 認証状態のセットアップ
    { name: 'setup', testMatch: /.*\.setup\.ts/ },
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
        // 保存した認証状態を読み込む
        storageState: 'playwright/.auth/user.json',
      },
      dependencies: ['setup'],
    },
  ],
});

実行時間の比較

| 方法 | 10 テスト実行時間 | |------|------------------| | 毎回ログイン | 約 90 秒 | | 認証状態再利用 | 約 30 秒 |


6. CI/CD への統合(GitHub Actions 例)

ワークフロー設定

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 10
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright Browsers
        run: npx playwright install --with-deps
      
      - name: Run Playwright tests
        run: npx playwright test
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
      
      - name: Upload test videos
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-videos
          path: test-results/
          retention-days: 7

並列実行の最適化

// playwright.config.ts
export default defineConfig({
  // CI では 4 並列、ローカルは CPU コア数に応じて自動
  workers: process.env.CI ? 4 : undefined,
  
  // シャード実行(複数マシンで分散実行)
  shard: process.env.CI
    ? {
        current: parseInt(process.env.SHARD_INDEX || '1'),
        total: parseInt(process.env.SHARD_TOTAL || '1'),
      }
    : undefined,
});

7. よくあるトラブルと対処法

トラブル 1:要素が見つからない

症状

Error: locator.click: Timeout 30000ms exceeded.

原因と対処

| 原因 | 対処法 | |------|--------| | 要素の読み込みが遅い | waitForSelector で明示的に待機 | | セレクタが不正確 | data-testid 属性を追加して特定 | | 動的なクラス名を使用 | getByRolegetByText を使用 |

// ❌ 避けるべき
await page.click('.btn-primary');

// ✅ 推奨
await page.getByRole('button', { name: '送信' }).click();
await page.getByTestId('submit-button').click();

トラブル 2:テストが不安定(Flaky Test)

対処法チェックリスト

  • [ ] waitForTimeout を使っていないか → waitForSelector に置き換え
  • [ ] ネットワークリクエストの完了を待っているか → waitForResponse を使用
  • [ ] 並列実行時のデータ競合はないか → テストデータを分離
  • [ ] アニメーション中に操作していないか → waitForLoadState('networkidle') を使用
// ✅ ネットワークリクエストを待つ
const responsePromise = page.waitForResponse(
  response => response.url().includes('/api/users') && response.status() === 200
);
await page.getByRole('button', { name: '読み込み' }).click();
await responsePromise;

トラブル 3:CI では失敗するがローカルでは成功

よくある原因

  • 環境変数の未設定BASE_URL などが CI で正しく設定されているか確認
  • ブラウザバージョンの違いnpx playwright install を CI で実行
  • タイムゾーンの違い:日時のテストは TZ 環境変数を統一

8. 実務で使えるテスト Tips

データのモック化

// API レスポンスをモック
await page.route('/api/users', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify([
      { id: 1, name: 'テストユーザー' },
    ]),
  });
});

スクリーンショット比較(Visual Regression Testing)

test('トップページのデザインが変わっていない', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixels: 100, // 許容する差分ピクセル数
  });
});

アクセシビリティテスト

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

test('アクセシビリティ違反がない', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

まとめ

Playwright による E2E テスト導入のポイントを整理します。

導入フェーズ別の推奨ステップ

| フェーズ | 実施内容 | 期間目安 | |----------|----------|----------| | Phase 1 | コアフロー(ログイン・購入)のみテスト化 | 1〜2 週間 | | Phase 2 | Page Object パターン導入・CI 統合 | 1 週間 | | Phase 3 | カバレッジ拡大・並列実行最適化 | 継続的 |

運用のコツ

  • テストは少数から始める:最初から 100% を目指さない
  • 失敗時のデバッグ情報を充実させる:動画・スクリーンショットを活用
  • 定期的にメンテナンス:UI 変更に合わせてテストも更新
  • チーム全体で責任を持つ:「テスト担当者」を作らない

Playwright は強力なツールですが、導入と運用には計画的なアプローチが必要です。この記事で紹介したパターンを参考に、プロジェクトに最適なテスト戦略を構築してください。


受託開発・自社開発での技術選定や実装支援が必要な場合は、Yureate にお気軽にご相談ください。

この内容について相談する他の記事を見る