この記事は約 5 分で読めます。
筆者プロフィール: ソフトウェアエンジニア。「知った気にならない。いつまでも学び続ける」を信条に、業務と個人開発の両輪で技術を磨いています。AI 駆動開発で複数の個人開発アプリを構築・運用中。
👉 ポートフォリオ: 筆者ホームページ
Playwright で E2E を書くと、フレーキー (不安定) な失敗 に悩まされます。本記事では、運用中の SaaS 「たすきば Knowledge Relay」 で蓄積した E2E_LESSONS (3,800 行) から、特に踏みやすい罠 8 点を整理します。
サービスの機能紹介・画面イメージ・コンセプトは公式プロダクトページをご覧ください。
👉 たすきば Knowledge Relay — 公式プロダクトページ
罠 1: sleep ベースの待機
// ✗ NG
await page.click('button');
await page.waitForTimeout(2000);
await expect(page.locator('div.result')).toBeVisible();
環境によって遅延が異なる。フレーキー。
// ✓ OK: 条件ベース待機
await page.click('button');
await expect(page.locator('div.result')).toBeVisible();
Playwright の toBeVisible() は 内部で条件待機。明示的な sleep は不要。
罠 2: セレクタが脆い
// ✗ NG: CSS セレクタが UI 変更で壊れる
await page.click('div > button:nth-child(3)');
// ✗ NG: text に依存
await page.click('button:has-text("保存する")');
// ✓ OK: data-testid で安定
await page.click('[data-testid="save-button"]');
data-testid を本番コードに付ける。UI 変更でも壊れない。
罠 3: テスト間でデータが残る
// ✓ OK: 各テストで独立した状態
test.beforeEach(async () => {
await resetDatabase();
});
各テストが独立、順序に依存しない。
罠 4: 並列実行時の競合
// ✗ NG: 並列で DB が競合
test.describe.parallel(...)
// ✓ OK: worker ごとに分離した DB
test.describe.serial(...)
// または storageState で worker isolation
Playwright の worker isolation で、各 worker が独立した状態。
罠 5: localStorage / Cookie の前提
// ✓ OK: 各テストで明示的にログイン
test.beforeEach(async ({ context }) => {
await context.clearCookies();
await loginAs('test@example.com');
});
Cookie をクリアしてから始める。
罠 6: モーダル / ダイアログの focus 競合
// ✗ NG: dialog が開く前に click
await page.click('button:has-text("削除")');
await page.click('button:has-text("確認")');
// ✓ OK: dialog 内のボタンを getByRole 経由で
await page.click('button:has-text("削除")');
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await dialog.getByRole('button', { name: '確認' }).click();
dialog 内のボタンを getByRole('dialog') 経由で指定。
罠 7: 非同期データの待機不足
// ✓ OK: ローディング完了を待つ
await page.goto('/projects');
await page.waitForLoadState('networkidle');
await expect(page.locator('text=テストプロジェクト')).toBeVisible();
networkidle で全ネットワーク完了を待つ。
罠 8: ローカル / CI で挙動が違う
| 原因 | 内容 |
|---|---|
| フォントレンダリング | Linux と macOS で差 |
| タイムゾーン | UTC vs Asia/Tokyo |
| 環境変数 | 違うシークレット |
| DB 初期状態 | リセット漏れ |
対策:
□ ローカルでも Docker で本番に近い環境
□ CI で fail したら、同じ環境で再現を試みる
□ 環境差分を最小化する設計
E2E 実行時間の管理
E2E は時間がかかる。全テスト 15 分以上だと CI 全体が遅い。
| 対策 | 内容 |
|---|---|
| 並列実行 | Playwright workers |
| 重要シナリオに絞る | 全 page をカバーしない |
| nightly ジョブ | 全シナリオは夜間に |
| PR では主要シナリオのみ | 高速化 |
CI 体験を維持するため、E2E は戦略的に書く。
Page Object Pattern の活用
class ProjectsPage {
constructor(public page: Page) {}
async goto() {
await this.page.goto('/projects');
}
async create(name: string) {
await this.page.click('button:has-text("新規")');
await this.page.fill('input[name=name]', name);
await this.page.click('button:has-text("作成")');
}
}
// テスト
const projects = new ProjectsPage(page);
await projects.goto();
await projects.create('テスト');
UI 変更時の修正範囲を Page Object に閉じ込められます。
Trace ファイルでデバッグ
E2E が fail したとき、Playwright Trace でデバッグ。
// playwright.config.ts
use: {
trace: 'on-first-retry',
}
| trace に含まれるもの | 用途 |
|---|---|
| スクリーンショット | 視覚的に確認 |
| DOM スナップショット | 構造の確認 |
| ネットワークログ | API リクエスト |
| console エラー | エラー詳細 |
CI fail 時に確認すれば、原因が即特定。
おわりに
| 罠 | 対策 |
|---|---|
| sleep ベース待機 | 条件ベース待機 |
| 脆いセレクタ | data-testid |
| テスト間データ残り | beforeEach でリセット |
| 並列競合 | worker isolation |
| Cookie 前提 | clearCookies |
| Dialog focus 競合 | getByRole('dialog') |
| 非同期待機不足 | waitForLoadState |
| 環境差分 | Docker + 環境変数管理 |
E2E の罠は無数だが、構造化することで対処可能 です。
本記事の罠と対策は、運用中の SaaS 「たすきば Knowledge Relay」 で蓄積したものです。
👉 たすきば Knowledge Relay — 公式プロダクトページ