Claude CodeがPlaywrightテストを生成するとき、なぜフラキーになるのか──CLAUDE.mdで防ぐアンチパターン完全ガイド

はじめに

「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-primarynth-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');

優先順位はgetByRolegetByLabelgetByTextgetByTestIdの順だ。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_navigatebrowser_clickbrowser_fillbrowser_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禁止を追加し、既存テストのwaitForTimeoutgrep -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がブラウザを直接操作しながらセレクターを確認してテストを生成するため、生成物の品質がさらに上がる。

コメント

タイトルとURLをコピーしました