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

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 属性を追加して特定 |
| 動的なクラス名を使用 | getByRole や getByText を使用 |
// ❌ 避けるべき
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 にお気軽にご相談ください。
