🎙️

【Playwright Test Agent】LLMを使ってE2Eテストの開発・保守コストを10分の1にできた、かも

に公開

株式会社MyVisionのフロントエンドエンジニアのsrkwです。
今回、社内でE2Eテストを新しく追加して運用対象に加えるにあたり、LLMを使って保守運用コストを大幅に削減することに成功した(かもしれない)ので、事例の共有をしたいと思います。

TL;DR

  • Claude CodeとPlaywright Test Agentsを使ってe2eテストのテストケース作成・テストコードの作成・修正をLLMにほぼ丸投げできるようになりました。
  • 局所的にLLMが生成するコードでは動作確認ができない場合は、推奨実装パターンをmarkdownファイルにまとめておくことで、テストコードの作成・修正エージェントが倣ってくれます。
  • 実装上アクセシブルでないためPlaywrightが操作対象を認識できないケースについては、実装を修正する必要があります
    • これはE2Eテストの有無を問わず自社のサイトのa11yを向上させるための施策として必要な工数となるでしょう。
  • とはいえ現状弊社のE2Eテストはそこまで数が多くなく、テスト内容も比較的シンプルであるため、大規模・大量のE2Eテストを回すようになった際に破綻する可能性もある。これを見越してタイトルは「かも」にしています。

説明しないこと

当記事では、Playwrightの記法や設定方法については説明しません。

やりたかったこと

E2Eテストを保守・運用するにあたり、以下のことをやらなくてよい体制を構築したいと考えていました。

  • 保守しやすいE2Eテストコードのための、テストコーディングガイドラインの整備・実装・レビューなど
  • 問題発生時の、Playwrightの記法・APIを熟知したうえでのテストコードの細かい調整

変数が多く壊れやすいE2Eテストを保守する上で、上記のコストが大きければ大きいほど、メインストリームの開発の速度が落ちてゆきます。また、正直Playwrightにとても詳しくなったとしても、そのスキルを応用できる場面が多くなく、学習対象としてのROIが高くないため、新しくPlaywright職人を増やすことに乗り気ではありませんでした。

このため、テストコードの追加・保守のコストを削りたいというのが、大元のニーズでした。
また、方針として、PlaywrightのE2Eテスト自体は--headedオプションなどを使ってテストしている内容を視覚的・簡単に確認できるので、テストコードの細かい記法自体は多少ブラックボックス化してもよいと判断し、LLMに丸投げしたいと考えていました。
こういったニーズ・方針に対して、そのものズバリというツールが公開されていたので、試してみました。

Playwright Test Agents

https://playwright.dev/docs/test-agents

25年10月にPlaywright ver1.56でリリースされた機能です。
内部仕様としては、指定したAIツールごとに適したエージェント構築用のmarkdownファイルが生成され、AIツール経由でそれらを呼び出すことで、テストケースの作成・テストコードの作成・テストコードの修正を移譲することができる、という仕組みになっています。

やったこと

弊社ではClaude CodeのMaxプランが全エンジニアに付与されており、Claude Codeを使って開発を行なっているメンバーが多いため、Claude Code環境用のエージェントを構築しました。

エージェント用markdown初期化

公式ドキュメントにある以下のコマンドで、markdownファイルを生成します。

npx playwright init-agents --loop=claude

これにより、以下のファイルが生成されます。

.claude/
└── agents/
    ├── playwright-test-planner.md
    ├── playwright-test-generator.md
    └── playwright-test-healer.md
.mcp.json
seed.spec.ts

生成されたファイルは、それぞれ以下のエージェントの構築用のプロンプトになります。

  • playwright-test-planner: アプリケーションを探索し、Markdown形式のテストケースを作成する
  • playwright-test-generator: Markdown形式のテストケースをPlaywrightのテストコードに変換する
  • playwright-test-healer: テストスイートを実行し、失敗したテストを自動的に修復する

これらを使ったワークフローの概観が以下です。

手元で試したところ、plannerについてはメンテ対象とするにはやや冗長なテストケースを書きがちな印象があったので、現状積極的な利用はしていません。
.claude.mcp.jsonはプロジェクトルートに配置する必要があるようなので、モノレポ環境ではファイルの配置場所に気をつけましょう。
また、生成される.mdファイル内のプロンプトは英語ですが、日本人のチーム内でメンテナンスするにはややダルいので、今回はこの時点で日本語化しました。

テストケースの作成

テストケースはMarkdown・自然言語で記述します。
(記事用に、テストケースはぼかしてあります。ステップ形式でTODOタスクを作成できるようなWEBアプリケーションを想定したテストケース例を以下に記載します。)

test-cases/create-todo-task.md
# テストケース: TODOタスクの作成
 
- `https://todo-task.example.com/create` にアクセス
  - Assert: 以下のリクエストが200レスポンスを得ること
    - `https://api.todo-task.example.com/graphql` へのリクエスト
    - リクエストボディに `logUserAccess` を含む
    - リクエストボディのvariables.inputに以下のプロパティを含むこと
      - sessionId: 何らかのUUID形式の文字列
- カテゴリ:「プログラミング」を選択
  - Assert: 画面に「期限」と表示されること
  - Assert: 画面に「カテゴリ」と表示されないこと
  - Assert: 以下のリクエストが200レスポンスを得ること
    - `https://api.todo-task.example.com/graphql` へのリクエスト
    - リクエストボディに `logUserStep` を含む
    - リクエストボディのvariables.inputに以下のプロパティを含むこと
      - sessionId: 何らかのUUID形式の文字列

Agentを使ってテストを作成

作成したテストケースをインプットとして、テストの作成を行います。
以下、プロンプトの例となります。

@agent-playwright-test-generator
 
{{ 作成したテストケースファイルのパス }}
上記のファイルに記入されたテストケースに基づいて、既存のテストコードを参考にしながらPlaywrightテストコードを生成してください。
生成されたテストコードは、同名のテストファイルを作成、もしくは更新する形で保存してください。
テストが完成したらテストコードの動作確認を行い、問題がある場合は適宜修正してください。また、動作確認は3回成功するまで繰り返し行い、テストが失敗するごとに修正を加えてください。

これをClaude Codeに入力し、しばらく待てばいい感じのE2Eテストが生えてきます。

LLMが生成するテストコードの評価

LLMが生成するPlaywrightテストコードですが、各エージェントの構成プロンプトが充実しているためか、特段「アンチパターンを踏みまくる」「やたら冗長なコードを書く」などの様子は見受けられませんでした。
テスト対象のアプリケーションの性質上こう書いて欲しい、などの要件がある場合は、後述するtips fileに記載するか、各エージェントのプロンプトを調整するとよさそうです。

また、各エージェントを使うことによるトークンの消費量についてですが、筆者の環境(Claude Code ver: 2.0.36, Playwright v1.56.1, Claude CodeおよびSubAgent(各エージェント)の両方でSonnet 4.5を利用, Claude Max)では「Test Agentを使ったら急にトークン枯渇が頻発した」というような事象は観測されませんでした。特段トークン消費量を食う構成というわけではないと評価しています。

Tips

テストコードの実装方法を固定

E2Eテストの作成・修正時、LLMにやらせると実装パターンがばらけるが、人間側は「こうすればうまくいく」という成功パターンを認識できている場合、Tips集のようなmarkdownファイルを作成して、generator healer の両方のプロンプトの冒頭に組み込んでおきます。

.claude/docs/playwright-e2e-tips.md
# Playwright E2Eテスト ベストプラクティス
 
このドキュメントは、todo-appプロジェクトでPlaywrightを使用したE2Eテストを作成する際のベストプラクティスをまとめたものです。
テストの作成・修正を行なう際は、以下のガイドラインに従ってください。
 
---
 
## タスク小カテゴリの選択
 
E2Eテストの中でタスク小カテゴリを選択する際は、以下の方法を使用してください。
 
```ts
  await test.step('タスク小カテゴリの入力を検証', async () => {
    // ReactSelectの入力フィールドを現在のステップ内から探してタスク小カテゴリ「フロントエンド」を入力
    const currentStep = page.locator('[data-current-step="true"]')
    const smallCategoryInput = currentStep.locator('input[type="text"]').first()
 
    // 入力フィールドが表示されて操作可能になるまで待機
    await smallCategoryInput.waitFor({ state: 'visible' })
    await smallCategoryInput.click()
    await page.waitForTimeout(300) // ReactSelectの内部状態が更新されるまで少し待機
    await smallCategoryInput.fill('フロントエンド')
 
    // ドロップダウンのオプションが表示されるまで待機してから「フロントエンド」を選択
    await page
      .getByRole('option', { name: 'フロントエンド', exact: true })
      .waitFor({ state: 'visible', timeout: 3000 })
    await page.getByRole('option', { name: 'フロントエンド', exact: true }).click()
```ts
.claude/agents/playwright-test-generator.md
あなたはPlaywright Test Generatorであり、ブラウザ自動化とエンドツーエンドテストのエキスパートです。
あなたの専門分野は、ユーザーインタラクションを正確にシミュレートし、アプリケーションの動作を検証する、堅牢で信頼性の高いPlaywrightテストを作成することです。
 
+ 作業開始前に`.claude/docs/playwright-e2e-tips.md`に記載されているPlaywright E2Eテストのベストプラクティスを必ず確認し、遵守してください。
.claude/agents/playwright-test-healer.md
あなたはPlaywright Test Healerであり、Playwrightテストの失敗のデバッグと解決を専門とするテスト自動化エンジニアのエキスパートです。あなたのミッションは、体系的なアプローチを使用して、壊れたPlaywrightテストを特定、診断、修正することです。
 
+ 作業開始前に`.claude/docs/playwright-e2e-tips.md`に記載されているPlaywright E2Eテストのベストプラクティスを必ず確認し、遵守してください。

こうすることで、generatorおよびhealerが各タスクの実行前にtipsを読み込み、実装結果の揺れや不要な試行錯誤によるトークンの浪費を抑えることができます。
また、tipsを1つのmarkdownファイルに切り出すことで、メンテナンス対象のmarkdownを減らすことを目指しています。

ただし、この方法を実践していてもtipsを読んでくれないor準拠されないことが稀にあり、完全に準拠させる方法は模索中です。もっとよい方法があるかもしれません。

環境変数を使った分岐

generatorのプロンプトに環境変数を示す構文とその取扱い方を明示することで、generatorに環境変数への参照を含むテストコードを生成させることができます。

.claude/agents/playwright-test-generator.md
+ # 環境変数とプレースホルダーの取り扱い
+ - テストケース内に `{{環境変数名}}` 形式のプレースホルダーがある場合、`testConfig.環境変数名` に変換する
+   - 例: `{{BASE_URL}}/forms/test``` `${testConfig.BASE_URL}/forms/test` ``
+ - プレースホルダーを使用している場合、テストファイルの冒頭に `import { testConfig } from '../utils/config'` を追加する
+ - これにより、local/staging等の環境切り替えを環境変数で制御できる

環境変数の実体自体は、tsファイルに定義しておきます。

tests/utils/config.ts
/**
 * E2Eテストの環境設定
 * 環境変数から設定を読み込み、テストコードで使用する
 */
export const testConfig = {
  BASE_URL: process.env.TODO_APP_BASE_URL || 'http://localhost:4000',
  APP_ENV: process.env.APP_ENV || 'local'
} as const

これにより、テストケースの中で環境変数を使った分岐を実現することができます。

test-cases/create-todo-task.md
  - リクエストボディのvariables.inputに以下のプロパティを含むこと
    - sessionId: {{APP_ENV}} === 'staging'の場合は何らかのUUID形式の文字列、{{APP_ENV}} === 'local'の場合は `test`
tests/create-todo-task.ts
    // sessionIdの検証(local環境では "test"、staging環境ではUUID形式)
    if (testConfig.APP_ENV === 'staging') {
      expect(input.sessionIdの検証).toMatch(uuidPattern)
    } else {
      expect(input.sessionIdの検証).toBe('test')
    }

テストケースごとの変数を使った分岐

前述の環境変数に加え、テストケースごとに定義した変数を使うこともできます。

test-cases/create-todo-task.md
+ ## 変数
+ 
+ テストケースについて、以下の値が含まれる文章については環境ごとの変数を展開して読み替えてください。
+ 
+ - {COMPLETED_PATH}: {{APP_ENV}} === 'staging'の場合は `completed`、{{APP_ENV}} === 'local'の場合は `completed/test`

自然言語・markdownにおける環境変数を使った分岐は割と冗長な書き方になってしまうことが多いので、多用する分岐については結果を変数化しておくとテストケースの文章がシンプルになります。

ブラウザ・デバイスの指定

E2Eテストの実行環境(SP・PCなど)をテストケースごとに指定することができます。

まずは playwright.config.ts でプロジェクトの設定をします。

playwright.config.ts
  /* ブラウザ/デバイスごとのプロジェクト設定 */
  projects: [
    {
      name: 'mobile',
      use: { ...devices['iPhone 14'] },
      testMatch: '**/*.mobile.spec.ts',
    },
    {
      name: 'desktop',
      use: { ...devices['Desktop Chrome'] },
      testMatch: '**/*.desktop.spec.ts',
    },
  ],

次に、各テストケースに実行環境を追記します。

test-cases/create-todo-task.md
# テストケース: TODOタスクの作成
 
+ ## 実行環境
+ 
+ SP
+ 
+ ## テストケース
+ 
 - `https://todo-task.example.com/create` にアクセス
  - Assert: 以下のリクエストが200レスポンスを得ること
    - `https://api.todo-task.example.com/graphql` へのリクエスト
    - リクエストボディに `logUserAccess` を含む
    - リクエストボディのvariables.inputに以下のプロパティを含むこと
      - sessionId: 何らかのUUID形式の文字列
 - カテゴリ:「プログラミング」を選択
  - Assert: 画面に「期限」と表示されること
  - Assert: 画面に「カテゴリ」と表示されないこと
  - Assert: 以下のリクエストが200レスポンスを得ること
    - `https://api.todo-task.example.com/graphql` へのリクエスト
    - リクエストボディに `logUserStep` を含む
    - リクエストボディのvariables.inputに以下のプロパティを含むこと
      - sessionId: 何らかのUUID形式の文字列

次に、テスト作成時のプロンプトに実行環境の読み取りと生成ファイルの拡張子の決定方法を追加します。

@agent-playwright-test-generator
 
{{ 作成したテストケースファイルのパス }}
上記のファイルに記入されたテストケースに基づいて、既存のテストコードを参考にしながらPlaywrightテストコードを生成してください。
生成されたテストコードは、同名のテストファイルを作成、もしくは更新する形で保存してください。
+ なお、テストケースファイルに実行デバイスとしてPCが指定されている場合はテストファイルの拡張子を`.desktop.spec.ts`、SPが指定されている場合はテストファイルの拡張子を`.mobile.spec.ts`としてください。
テストが完成したらテストコードの動作確認を行い、問題がある場合は適宜修正してください。また、動作確認は3回成功するまで繰り返し行い、テストが失敗するごとに修正を加えてください。

これにより、生成されるテストファイルの拡張子が.mobile.spec.ts.desktop.spec.tsに割り振られ、テストケースごとに指定された実行環境でのテスト実行が可能になります。

まとめ

Playwright TestAgentと前述のTipsを使って、自然言語で記載したテストケースからE2Eテストを自動生成できるようになりました。
前職でE2E職人をやっていた身として当時と比較すると、E2Eテストの実装・修正コストが体感で10%未満に減っているので、とても満足しています。
冒頭のTL;DRでも述べた通り、今後テストが大規模化するにあたって破綻する可能性はありますが、しばらく様子を見ていこうと思っています。

MyVision技術ブログ

Discussion