Playwright MCPによるE2Eテストコードの生成 第1回 最小限の指示で生成する、保守性の高いE2Eテストコード
このシリーズでは、Claude CodeとPlaywright MCPを連携させたE2Eテストコードの生成方法を紹介します。第1回は、Claude in Chromeとの違いを整理し、セマンティックロケーターが自然に選ばれる仕組みと、保守性を高めるスキルの定義を解説します。
はじめに
このシリーズでは、Claude CodeとPlaywright MCPを連携させて、PlaywrightベースのE2Eテストコードを生成する方法を解説します。
以前、同じことをClaude in Chromeで行う方法を紹介しました。そこでは、壊れにくいテストコードにするために、UI要素の特定方法をスキルで細かく指示する必要がありました。
Playwright MCPは、E2EテストフレームワークのPlaywrightをMCPサーバーとして動かすツールです。ページの情報を要素の役割や名前で整理した形でClaudeに渡すので、UI要素の特定方法を細かく指示しなくても、壊れにくいテストコードを生成しやすくなっています。本シリーズでは、Claude in Chromeとの違いにも触れながら、その仕組みと使い方を解説します。
また、本記事の解説に使用したサンプルプログラムを、次のリポジトリで公開しているので併せて参考にしてください。
サンプルリポジトリ
Claude in Chromeとの比較
Playwright MCPをClaude in Chromeと比べると、費用・対応ブラウザ・セッション管理などで、有利な点が目立ちます。
- 費用:Claude in ChromeはClaude.aiの有料契約が必要ですが、Playwright MCPはオープンソースで無料です
- マルチブラウザ対応:Playwright MCPはChrome・Firefox・WebKit・Edgeに対応しています。Claude in ChromeはChromeやEdgeなど、Chromium系のブラウザに限られます
- ブラウザセッション:Claude in Chromeは常時起動しているChromeに接続します。Playwright MCPはデフォルトで独立したブラウザを起動しますが、既存のChromeに接続するモードや、ログイン状態を保持する永続モード・毎回クリーンな状態で起動する分離モードなど、用途に応じた使い分けができます
さらに、テストコード生成という目的において特に重要な違いがあります。Playwright MCPはアクセシビリティスナップショットという仕組みでページを認識しており、これがセマンティックロケーターの採用に直結します。次節から詳しく説明します。
補足:MCPの追加とコンテキスト消費
MCPサーバーを追加するとコンテキストを圧迫するのではと気にする方もいるかもしれませんが、Claude CodeにはTool Searchという機能があり、ツール定義は利用時まで遅延ロードされます。設定で常時ロードに変えることもできますが、既定は遅延ロードです。これはClaude in Chromeと連携する場合も同様で、どちらも導入によるコンテキストの圧迫は起きません。
セマンティックロケーターによる保守性
E2EテストフレームワークにPlaywrightを採用する理由の1つに、保守性の高いテストコードを書きやすい点があります。その鍵は、要素の特定方法にあります。
Claude Codeに、ロケーターの選択条件を指定せずにテストコードを生成させると、次のようなCSSクラス名依存のロケーターになりがちです。
クラス名依存のロケーター
page.locator('.btn-submit').click()
クラス名はユーザーには見えない実装の詳細です。スタイル調整でクラス名が変わると、UIの振る舞いに影響がないにもかかわらずテストが壊れます。
これに対しPlaywrightには、ユーザーが目にするラベルなどを基準に要素を特定するAPIが用意されています。これをセマンティックロケーターと呼び、たとえば次のように書くことができます。
セマンティックロケーターの例
page.getByRole('button', { name: '保存' }).click()
このコードはbuttonロールと「保存」というラベルを持つ要素を特定しています。「保存」というラベルが変わらない限りテストは壊れませんし、壊れる理由にも納得ができます。Playwrightはこのセマンティックロケーターを推奨手段として設計に組み込んでおり、公式ドキュメントでも最初に紹介しています。
Playwright MCPを使うと、このセマンティックロケーターが、特に指示しなくても選ばれるようになります。Claude in Chromeではスキルで細かく指示する必要がありましたが、その指示はいりません。理由は、Playwright MCPがページを認識する仕組みであるアクセシビリティスナップショットにあります。
アクセシビリティスナップショット
Playwright MCPがブラウザを操作する際、ページの状態はDOMツリーではなくアクセシビリティスナップショットとして取得されます。これはスクリーンリーダーが見るのと近い構造で、要素のロールとアクセシブルネームを中心に構造化されたテキスト表現です。
アクセシビリティスナップショットの例
- heading "todos" [level=1]
- textbox "What needs to be done?" [ref=e5]
- listitem:
- checkbox "Toggle Todo" [ref=e10]
- text: "Buy groceries"
DOMツリーと異なり、divやspanのような意味のない要素は含まれず、各インタラクティブ要素には一意のrefが付与されます。ClaudeはこのrefをMCPツールに渡して、対象の要素を操作します。
このスナップショットにはロールとアクセシブルネームが構造化された形で含まれており、Claudeが受け取る情報がすでにセマンティックな構造になっているため、getByRole()やgetByLabel()といったセマンティックロケーターが自然に選ばれます。
セマンティックロケーターが自然に生成される
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email')
これがスキルで指示しなくてもセマンティックロケーターが採用される理由です。Claude in Chromeで必要だったルール定義をPlaywright MCPが代わりに担っている、と言い換えることもできます。
なお、Playwright MCPはPlaywrightそのものをMCPサーバーとして動かしているため、このロケーターを実際に実行してその結果を確認することができます。
使ってみる
E2Eテストコードの生成をする前に、まずPlaywright MCPを使ってブラウザを操作してみましょう。設定は.mcp.jsonに数行記述するだけです。
プロジェクトルートの.mcp.jsonに以下を記述してください。
.mcp.json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest"]
}
}
}
設定後、Claude Codeに次のように指示してみます。対象はPlaywrightが動作確認用に公式提供しているToDoアプリのデモサイトです。
プロンプト例
https://demo.playwright.dev/todomvc に移動して、ToDoを3つ追加してください。
Playwright MCPが起動してブラウザが開き、Claudeが操作を進めます。各ステップでアクセシビリティスナップショットが取得され、Claudeはその内容をもとに次の操作を判断します。自然言語で指示するだけでブラウザが動き、使い始めのハードルの低さを実感できます。
E2Eテストコードを書かせる
では実際にテストコードを生成してみます。CodeGridのサイトを対象に、未ログイン状態での有料記事ページの挙動をテストします。まずスキルなしで、次のようなプロンプトを渡してみます。
プロンプト例
Playwright MCPを使って、01-guest-user-test.spec.ts にテストを書いてください。
未ログインでの有料記事にアクセス時のテスト
- 有料記事ページ https://www.codegrid.net/articles/2026-meta-framework-selection-3/ にアクセス
- ヘッダー右メニュー「購読する」「ログイン」表示があること
- 記事の目次下に「この記事を読むには購読の手続きが必要です」の表示があること
- 「購読する」をクリックしたら、ページ遷移し「購読のためのGoogleアカウントを選択」ボタンが表示されること
Playwright MCPがブラウザを操作しながら処理が進み、筆者の環境では次のようなテストコードが生成されました。
生成されたテストコード
import { test, expect } from "@playwright/test";
test.describe("未ログインでの有料記事アクセス", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/articles/2026-meta-framework-selection-3/");
});
test("ヘッダー右メニューに「購読する」「ログイン」が表示される", async ({
page,
}) => {
await expect(page.getByRole("link", { name: "購読 する" })).toBeVisible();
await expect(
page.getByRole("link", { name: "ログイン", exact: true })
).toBeVisible();
});
test("記事の目次下に購読促進メッセージが表示される", async ({ page }) => {
await expect(
page.getByRole("heading", {
name: "この記事を読むには 購読の手続きが必要です",
})
).toBeVisible();
});
test("「購読する」をクリックするとGoogleアカウント選択ボタンが表示される", async ({
page,
}) => {
await page.getByRole("link", { name: "購読 する" }).click();
await expect(
page.getByRole("button", { name: "購読のためのGoogleアカウントを選択" })
).toBeVisible();
});
});
スキルで指示しなくても、getByRole()やgetByLabel()といったセマンティックロケーターが自然に採用されています。ただし、実践で使うにはもう少し手を加えたほうがよい点もあります。次節ではその対応をスキルとして定義します。
保守性を意識したテストコードの生成
スキルなしでもセマンティックロケーターが採用されることは確認できましたが、実践で使うには、さらに次の点も押さえておく必要があります。詳しくはClaude in Chromeの記事で解説していますが、ここではPlaywright MCPでの違いに絞って説明します。
部分一致を許可しない:テキストの一致判定はデフォルトで部分一致です。同じテキストを含む要素がページに追加されると、ロケーターが複数の要素にヒットしてテストが壊れる可能性があります。そのためexact: trueを指定して完全一致を条件にします。Claude in Chromeの記事では正規表現(アンカー付き)を採用しましたが、これはquerySelectorAll()で一意性を検証する際に同じ判定条件を流用する必要があったためです。Playwright MCPではロケーターをそのまま実行して確認できるので、exact: trueで素直に書けます。
コンテナによる絞り込み:exact: trueで完全一致にしても、機能追加などで同じテキストを持つ要素が別のエリアに追加されれば複数ヒットになります。現時点で一意に特定できていても、将来的なリスクに備えて対象要素を含む親要素(コンテナ)でエリアを絞っておくことを基本とします。
一致件数の検証:アクセシビリティスナップショットではrefで一意に要素を特定していますが、テストコードに書き出す際にセマンティックロケーターに変換される以上、複数の要素にヒットしないとは言い切れません。前述のとおりロケーターはそのまま実行できるので、該当件数を返すcount()というAPIでヒット件数を得て、ロケーターを確定する前に一意に特定できているかを確かめます。
これらをスキルとして定義することで、Claudeへの毎回の指示が不要になります。セマンティックロケーターの採用はPlaywright MCP側が担う分、Claude in Chromeのスキルよりシンプルな定義で済みます。
スキルを使って生成したテストコードは次のようになりました。
スキルを使って生成されたテストコード
import { test, expect } from '@playwright/test';
test('未ログインでの有料記事にアクセス時のテスト', async ({ page }) => {
await page.goto('/articles/2026-meta-framework-selection-3/');
const header = page.locator('.cg-GlobalHeader');
await expect(
header.getByRole('link', { name: '購読 する', exact: true }),
).toBeVisible();
await expect(
header.getByRole('link', { name: 'ログイン', exact: true }),
).toBeVisible();
const article = page.locator('article');
await expect(
article.getByRole('heading', {
name: 'この記事を読むには 購読の手続きが必要です',
exact: true,
}),
).toBeVisible();
await header.getByRole('link', { name: '購読 する', exact: true }).click();
const payment = page.locator('.cg-Payment');
await expect(
payment.getByRole('button', {
name: '購読のためのGoogleアカウントを選択',
exact: true,
}),
).toBeVisible();
});
スキルなし版と比べると、コンテナによる絞り込みとexact: trueが加わっています。スキルを定義するひと手間はありますが、保守性を意識したテストコードが生成できる状態になります。
まとめ
今回は、Playwright MCPでE2Eテストコードを生成する方法を紹介しました。テストコードに採用するセマンティックロケーターを生成段階でそのまま実行できるため、精度の高いコードが生成できることがわかりました。
次回は、自然言語による指示が破綻するケースを扱います。長い手順の指示が必要になる、あるいは対象を指す言葉がない。そうしたUIでもE2Eテストコードを生成する方法を紹介します。