モダンE2Eテスト入門:Playwrightの基礎から実践まで
Selenium比較・CI統合・デバッグ手法まで網羅 - 実際に動くコード例で学ぶ実践ガイド

Playwrightのインストールで止まっていませんか?ここからが本番です!
前回の記事では、Playwrightのセットアップ方法について解説しました。 しかし、ツールはインストールしただけでは意味がありません。
今回は、その続きとして 「モダンE2Eテスト入門:Playwrightの基礎から実践まで」 と題し、Playwrightを実際にどう使いこなし、チームの品質保証をどう向上させるのかを、具体的なコードと共に掘り下げていきます。
フロントエンド開発において、E2E(End-to-End)テストは「最後の守り」。 しかし現場ではよくこんな声を聞きます。
- 「設定が面倒でチームに浸透しない」
- 「テストが遅くてCIが詰まる」
- 「動いたり動かなかったりで信用できない」
この課題を解決するのが、Microsoft 主導で開発されている Playwright です。 この記事では「学びながら実際に動かせる」流れで、Playwright の基本と応用を段階的に解説します。
さらに単なるブラウザー操作だけでなく、「テストとして何を検証するのか」「どう成果物を残すのか」 にも焦点を当てます。
1. なぜ Playwright なのか?
Seleniumとの比較で理解する
項目 | Playwright | Selenium |
---|---|---|
実行速度 | 高速(ブラウザと直接通信)+自動待機 | やや遅め。明示的な待機(WebDriverWait )が必要 |
安定性 | 要素がクリック可能になるまで自動待気 → Flakyを減らす | 明示的な待機やリトライ戦略が必須 |
機能 | スクリーンショット比較、APIモック、Trace Viewer が標準 | サードパーティ連携に依存する部分が多い |
Selenium の利点も押さえておこう
- 幅広い言語対応:Java / Python / C# / Ruby …
- ブラウザの幅広さ:IEや古いSafariなどレガシー環境では有効
- 成熟したエコシステム:大規模 Grid 環境の実績
→ 新規・モダン環境 → Playwright、レガシー資産 → Selenium。
2. Step 0:セットアップ
npm init -y npm init playwright@latest npx playwright install --with-deps
3. playwright.config.ts
の基本設定
テストを書く前に、設定ファイル playwright.config.ts
を見てみましょう。ここで基本設定を行うことで、テストコードをより簡潔かつ強力にできます。
import { defineConfig } from '@playwright/test'; export default defineConfig({ // テストファイルが格納されているディレクトリ testDir: './tests', // テストのタイムアウト時間(ミリ秒) timeout: 30 * 1000, // page.goto('/') のような相対パスの基準となるURL baseURL: 'http://localhost:3000', // テスト実行時の共通設定 use: { // ヘッドレスモードで実行(CIではtrueが基本) headless: true, // スクリーンショットを撮影するタイミング screenshot: 'only-on-failure', // Traceファイルを記録するタイミング(失敗時に記録が非常に有用) trace: 'retain-on-failure' } });
ポイント
trace: 'retain-on-failure'
を設定しておくと、失敗したテストの操作履歴がすべて記録され、デバッグが劇的に楽になります。
4. Step 1:ナビゲーションテスト
解説: 最初の一歩は「アプリが正しく立ち上がるか」を確認することです。ページが表示され、タイトルが期待通りであれば、最低限のデプロイ確認ができます。
tests/navigation.spec.ts
import { test, expect } from '@playwright/test'; test('ページアクセステスト', async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle(/My App/); });
5. テストの実行と結果の確認
解説:
最初のテストコードが書けたので、実行してみましょう。Playwrightはコマンド一つで、tests/
ディレクトリ内のすべてのテストを実行してくれます。
# これでテストが実行されます! npx playwright test
実行結果の確認:HTMLレポート
Playwrightの真価はHTMLレポートにあります。
# レポートをブラウザで開く npx playwright show-report
このレポートでは、各テストの成功/失敗、実行時間、そして設定が有効であれば失敗時のTraceまで確認できます。テストが失敗したとき、このレポートを見れば、どこで何が起きたのかが一目瞭然です。
6. Step 2:フォーム操作テスト
解説: ユーザーの操作を再現し「正しい入力が受け付けられ、送信後に完了メッセージが表示される」ことを確認します。
tests/form.spec.ts
test('フォーム入力テスト', async ({ page }) => { await page.goto('/contact'); // ページがロードされるまで待機 await expect(page.getByRole('heading', { name: 'お問い合わせ' })).toBeVisible(); await page.getByLabel('お名前').fill('テスト太郎'); await page.getByLabel('メールアドレス').fill('test@example.com'); await page.getByRole('button', { name: '送信' }).click(); await expect(page.getByText('お問い合わせありがとうございます')).toBeVisible(); });
コラム:なぜ
getByRole
なのか?page.getByRole('button')
のような「Locator」は、id
やclass
といった変わりやすいものではなく、ユーザーがどう要素を認識するかに基づいて選択するため、DOMの構造変更に強く、壊れにくいテストを書くことができます。
7. Step 3:APIモックテスト
解説: 外部APIが不安定でも「UIがどう振る舞うか」をテストできます。「サーバーエラー時」や「ネットワーク障害時」の挙動を検証可能です。
tests/api-mock.spec.ts
test('APIエラー時の表示テスト', async ({ page }) => { // 500エラーを返すモック await page.route('**/api/submit', route => { route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Internal Server Error' }), }); }); await page.goto('/contact'); await page.getByLabel('お名前').fill('テスト太郎'); await page.getByLabel('メールアドレス').fill('test@example.com'); await page.getByRole('button', { name: '送信' }).click(); await expect(page.getByText('サーバーエラーが発生しました')).toBeVisible(); }); test('ネットワーク障害時の表示テスト', async ({ page }) => { // サーバーダウンなどを想定したネットワークエラー await page.route('**/api/submit', route => route.abort()); await page.goto('/contact'); await page.getByLabel('お名前').fill('テスト太郎'); await page.getByLabel('メールアドレス').fill('test@example.com'); await page.getByRole('button', { name: '送信' }).click(); await expect(page.getByText('サーバーエラーが発生しました')).toBeVisible(); });
8. Step 4:実践例:認証フロー
解説: ログイン成功/失敗の両パターンをテストします。「失敗時に正しいエラーが出ない」「成功後に正しいページに遷移しない」といったバグは頻出するため、必ずペアで書くことが重要です。
tests/auth.spec.ts
test.describe('ユーザー認証フロー', () => { test.beforeEach(async ({ page }) => await page.goto('/login')); test('正常ログイン', async ({ page }) => { await page.locator('#email').fill('user@example.com'); await page.locator('#password').fill('password123'); await page.getByRole('button', { name: 'ログイン' }).click(); await expect(page).toHaveURL(/.*dashboard/); }); test('不正ログイン', async ({ page }) => { await page.locator('#email').fill('invalid@example.com'); await page.locator('#password').fill('wrongpassword'); await page.getByRole('button', { name: 'ログイン' }).click(); await expect(page.getByText('メールアドレスまたはパスワードが正しくありません')).toBeVisible(); }); });
9. 開発を加速させるデバッグ手法
テストが失敗したとき、以下のツールが開発の助けになります。
コマンド/コード | 用途 |
---|---|
await page.pause(); | テストをその場で一時停止し、ブラウザの状態で要素の確認や操作ができます。 |
npx playwright test --debug | Playwright Inspectorを起動し、ステップ実行やLocatorの評価ができます。 |
npx playwright codegen [URL] | ブラウザ操作を記録し、テストコードを自動生成してくれます。 |
10. パフォーマンスと安定性向上のテクニック
「テストが遅い」「時々失敗する」といった問題に対処するテクニックです。
- 並列実行:
npx playwright test --workers=4
のように、テストを並列で実行し、全体の時間を短縮します。 - テストケースの絞り込み: E2Eテストは重要なユーザーフロー(認証、購入など)に絞り、コンポーネントの網羅的なテストは単体テストに任せるなど、役割を分担します。
- Page Object Model (POM) の活用: 大規模なプロジェクトでは、ページごとの要素や操作をクラスとしてカプセル化するPOMデザインパターンを導入すると、テストの再利用性とメンテナンス性が向上します。
11. CI/CD統合
GitHub Actionsに組み込むことで、プルリクエストごとにテストを自動実行し、品質を担保します。
.github/workflows/playwright.yml
name: Playwright Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run Playwright tests # workersフラグで並列実行数を指定 run: npx playwright test --workers=4 - name: Upload report uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30
12. 実装時によくある課題と対策
実際にテストを作成・運用する際によく遭遇する問題とその解決策をまとめました。
Locatorが要素を認識しない
症状: getByLabel()
でTimeoutError が発生
原因: HTMLの label
要素と for
属性の関連付けが不完全
対策: IDセレクター locator('#id')
に変更
// 理想的だが環境によって動作しない場合がある await page.getByLabel('お名前').fill('テスト太郎'); // より確実な方法 await page.locator('#name').fill('テスト太郎');
APIモックが期待通りに動作しない
症状: モック設定したのにエラーメッセージが表示されない
原因: route.fulfill()
の形式が不正確
対策: contentType
と body
を適切に設定
// 動作しない場合がある await page.route('**/api/submit', route => { route.fulfill({ status: 500, json: { message: 'Error' }, // この形式は非推奨 }); }); // 確実に動作する形式 await page.route('**/api/submit', route => { route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'Error' }), }); });
テストが不安定(時々失敗する)
症状: 同じテストが成功したり失敗したりする
原因: 非同期処理の完了を適切に待機していない
対策:
- タイムアウト値の調整
- ページ要素の表示確認を追加
- 適切な待機処理の実装
// 不安定なテスト test('フォーム送信', async ({ page }) => { await page.goto('/contact'); await page.getByRole('button', { name: '送信' }).click(); await expect(page.getByText('完了')).toBeVisible(); }); // 安定化されたテスト test('フォーム送信', async ({ page }) => { await page.goto('/contact'); // ページの準備完了を確認 await expect(page.getByRole('heading', { name: 'お問い合わせ' })).toBeVisible(); await page.getByRole('button', { name: '送信' }).click(); // タイムアウトを適切に設定 await expect(page.getByText('完了')).toBeVisible({ timeout: 10000 }); });
13. まとめ:Playwrightをチームの味方に
- シンプルなナビゲーションテスト → デプロイ直後の健全性をチェック
- フォーム操作やAPIモック → ユーザー体験の完全再現
- スクリーンショットやTrace → テスト結果を「証跡」として活用
- CI/CD統合と並列実行 → 品質保証が自動化され、レビュー効率も向上
→ Selenium の利点も理解し、プロジェクトに最適な選択を。
よく使う Playwright アサーション早見表
すべて
import { test, expect } from '@playwright/test'
前提。これらはすべて自動リトライ付きのため、原則として明示的な待機処理は不要です。
1. UI の表示状態を確認する
await expect(locator).toBeVisible(); // 要素が表示されている await expect(locator).toBeHidden(); // 要素が非表示 await expect(locator).toBeEnabled(); // 有効状態(クリック可能など) await expect(locator).toBeDisabled(); // 無効状態
2. テキストや入力値を検証する
await expect(locator).toHaveText('保存しました'); // 完全一致 await expect(locator).toContainText('エラー'); // 部分一致 await expect(locator).toHaveValue('user@example.com'); // 入力欄の値 await expect(locator).toHaveCount(10); // 要素の個数
3. 属性やスタイルを検証する
await expect(locator).toHaveAttribute('alt', 'ロゴ'); await expect(locator).toHaveClass(/modal-open/); // 正規表現で部分一致 await expect(locator).toHaveCSS('color', 'rgb(255, 0, 0)');
4. ページ遷移やタイトルを検証する
await expect(page).toHaveURL(/\/dashboard$/); await expect(page).toHaveTitle(/My App/);
5. スクリーンショット比較(ビジュアル回帰テスト)
await expect(page).toHaveScreenshot('home.png');
6. ネットワークレスポンスを検証する
const response = await page.waitForResponse(/\/api\/submit/); expect(response.status()).toBe(200);