はじめに
「Claude Codeで書いてもらったE2EテストがCIでよく落ちる」という声は珍しくない。原因はAIの問題ではなく、制御していないことの問題だ。Claude CodeにPlaywrightのベストプラクティスを伝えるCLAUDE.mdがなければ、CSSクラス依存のセレクターやwaitForTimeoutが出てくる。TDDのユニットテスト記事では「テストを先に書かせる」文化設計を扱ったが、本記事はE2E固有の「最初から壊れにくいテストを生成させる」CLAUDE.md設計に絞る。
Claude CodeがE2Eテストで陥りやすいパターン
セレクター:CSSクラス依存からgetByRoleへ
Playwright公式は「CSSクラスやXPathは最後の手段」としている。デザイン変更でクラス名が変われば即壊れるからだ。Claude Codeはデフォルトで.btn-primaryやnth-childを使いがちだ。
// ❌ CSSクラス依存(クラス名変更で即壊れる)
await page.locator('.btn-primary-submit-v2').click();
await page.locator('#login-form > div:nth-child(2) > input').fill('test@example.com');
// ✅ ロールベース(デザイン変更に強い)
await page.getByRole('button', { name: 'ログイン' }).click();
await page.getByLabel('メールアドレス').fill('test@example.com');
優先順位はgetByRole → getByLabel → getByText → getByTestIdの順だ。CSSとXPathは禁止とCLAUDE.mdに明記する。
Wait:固定待機からWeb-First Assertionへ
Playwright公式は「waitForTimeoutを本番コードで使わないこと。固定待機は本質的にフラキーだ」と明記している。CIサーバーの負荷が高いときに間に合わなくなる。
// ❌ 固定待機(CIで不安定になる代表例)
await page.waitForTimeout(3000);
await page.click('#submit-button');
// ✅ Web-First Assertion / APIレスポンス待機
await page.getByRole('button', { name: '送信' }).click();
await expect(page.getByText('送信完了')).toBeVisible();
// APIを待つ場合: await page.waitForResponse(r => r.url().includes('/api/submit'))
構造:インライン記述からPage Object Modelへ
Claude Codeはデフォルトで1ファイルにすべての操作をインライン記述する。ログイン操作が複数テストに重複し、UI変更時の修正コストが爆発する。Page Object Modelを使うとセレクターと操作をページクラスに封じ込められる。
// ✅ Page Object Model(e2e/pages/login.page.ts)
export class LoginPage {
constructor(readonly page: Page) {}
async login(email: string, password: string) {
await this.page.goto('/login');
await this.page.getByLabel('メールアドレス').fill(email);
await this.page.getByLabel('パスワード').fill(password);
await this.page.getByRole('button', { name: 'ログイン' }).click();
await expect(this.page.getByText('ダッシュボード')).toBeVisible();
}
}
// tests/purchase.spec.ts では loginPage.login() を呼ぶだけ
ディレクトリはe2e/pages/・e2e/fixtures/・e2e/tests/に分離する。
CLAUDE.mdでPlaywrightの品質を強制する
上記3パターンとテスト独立性・CI設定をまとめてCLAUDE.mdに書く。
## E2Eテスト(Playwright)の規則
### セレクター優先順位
getByRole > getByLabel > getByText > getByTestId
禁止: CSS / XPath(DOM構造依存)
### 待機処理
- page.waitForTimeout() は禁止
- page.waitForLoadState() / page.waitForResponse() / Web-First Assertionを使う
### アーキテクチャ
- Page Object Model必須: e2e/pages/*.page.ts にページクラスを作成
- テストファイル(*.spec.ts)内にgetBy*を直接書かない
### テスト独立性
- test.beforeEach()で状態をリセット
- 認証はstorageState(playwright/.auth/)を使う
### playwright.config.ts(CI環境)
- retries: CI ? 2 : 0 / trace: 'on-first-retry' / screenshot: 'only-on-failure'
Playwright MCP × Claude Codeで「動くテスト」から書き始める
Microsoft公式の@playwright/mcpをClaude Codeに設定すると、ブラウザを直接操作しながらテストを生成できる。
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp"]
}
}
}
~/.claude/settings.jsonに追加する。browser_navigate・browser_click・browser_fill・browser_screenshot等のツールが使えるようになる。
playwright codegen(実操作を録画してコード化)はCSSセレクターが出やすい。Playwright MCPはCLAUDE.mdで制御すればgetByRoleを優先させられるため、フラキーリスクが低い。Claude Codeへの指示例は「Playwright MCPでブラウザを操作しながら確認して、getByRoleを最優先でPOMに分離してE2Eテストを生成して」と一文で伝えるだけでよい。
チームのE2EテストをClaude Code文化に根付かせる
CLAUDE.mdにPlaywrightルールを書けば、チーム全員のClaude Codeが同じ品質基準で動く。1週目でCLAUDE.mdにセレクター規則とwaitForTimeout禁止を追加し、既存テストのwaitForTimeoutをgrep -r "waitForTimeout" e2e/で検出して置き換える。2週目でPOMのディレクトリ構造を定義して新規テストから適用する。GitHub Actionsへの--fail-on-flaky-tests追加とCIのE2Eレポート週次確認はその後に加える。
ユニットテスト(TDD)とE2Eテストは補完関係にある。ユニットが全部パスしても本番で壊れたケースをE2Eが防ぐ。ユニットテストはTDDパターン、E2Eは本記事のCLAUDE.md設定。Claude Codeを使うならこの組み合わせが効く。
まとめ
フラキーテストの根本原因はCSSセレクター・固定待機・インライン記述だ。CLAUDE.mdに「getByRole優先・waitForTimeout禁止・Page Object Model必須」を明記してほしい。Playwright MCPを追加すればClaude Codeがブラウザを直接操作しながらセレクターを確認してテストを生成するため、生成物の品質がさらに上がる。

コメント