Claude CodeでSeleniumのテストを移行する — Playwrightの待機とロケータに置き換え
SeleniumのテストをPlaywrightへ移すとき、暗黙的waitとCSSセレクタの置き換え方。Claude Codeに旧テストを読ませ、Playwright MCPで実画面を見て、移行前後で検証が減っていないかを突き合わせます。
SeleniumのテストをPlaywrightへ移すと、いちばん事故が起きやすいのは書き換えそのものではなく、書き換えた結果「通るけれど何も確かめていないテスト」ができることです。Claude Codeに旧テストを読ませて新テストを書かせるのは速い一方、assertionが落ちても気づきにくい。本記事は、待機とロケータの置き換え規則を先に決め、Playwright MCPで実画面を見て、移行前後の検証を突き合わせるところまでを手順にします。
Playwright MCPの導入はPlaywright MCPサーバーの使い方に、Selenium側のMCPはSelenium MCPをClaude Codeに繋ぐにあります。ここでは両方を使った「移す作業」に絞ります。
前提:Playwright Testとconfigの準備
移行後のコードはpage.goto('/login')のように相対パスで書くため、Playwright Testの導入と、configのbaseURLが前提になります。移行前のSeleniumのコードは${baseUrl}/loginと自前で連結していたので、ここが最初の差です。Node.jsが入った環境で、プロジェクトのルートから次を実行します。
npm init playwright@latestTypeScriptかJavaScriptか、テストを置くフォルダ名などを聞かれるので、移行先のテスト構成に合わせて選びます。生成されるplaywright.config.tsに、baseURLと、expectの待ち時間を次のように書きます。
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// page.goto('/login') がこのURLからの相対パスになる
baseURL: 'http://localhost:3000',
},
expect: {
// expect() が条件を待つ最大時間。既定は5000ms
timeout: 5000,
},
});baseURLは移行前のbaseUrl変数に当たる値で、ステージング環境などテスト対象のURLを入れます。操作(click()など)の待ち時間はuse.actionTimeoutで決められ、既定は0(制限なし)です。
移行で変わるのは待機の考え方とロケータの選び方
Seleniumでは、要素が出るまでの待ちをテスト側が書きます。公式ドキュメントは、固定のsleepは短すぎれば失敗し、長すぎればセッションが長くなると述べ、代わりに暗黙的waitと明示的waitの2つを挙げています。暗黙的waitはセッション全体に効く設定で、既定値は0です。明示的waitは、条件が真になるまで一定間隔で確かめるループです。
Playwrightは待機を操作の中に持ちます。たとえばclickの前に、要素が1つに決まる、見えている、動きが止まっている、他の要素に隠されていない、有効である、の5つを自動で待ちます。時間内に揃わなければTimeoutErrorで失敗します。つまり、旧テストにある待機コードの多くは「書き換える」のではなく「消す」対象です。
| 観点 | Selenium | Playwright |
|---|---|---|
| 待機の置き場所 | Seleniumテスト側に書く(暗黙的wait・明示的wait) | Playwright操作とexpectの中に内蔵 |
| 要素が複数ヒットしたとき | Selenium旧テストの書き方による | Playwrightstrict違反でエラーになる |
| 推奨される探し方 | SeleniumID・CSS・XPathが中心 | Playwrightrole・label・textなど利用者から見える属性 |
| 検証の書き方 | Selenium値を取り出してassertする | Playwright自動リトライするexpectを使う |
要素が複数ヒットしたときの挙動は、移行で最も表に出る差です。Playwrightのロケータは厳格で、複数の要素に当たる操作は例外になります。旧テストが複数の要素のうち1つを選んで通っていた箇所は、ここで初めて見つかります。first()やnth()で黙らせることもできますが、ページが変わると別の要素を指しかねないため、推奨されていません。
置き換え規則を先にCLAUDE.mdへ書く
Claude Codeに「Playwrightに書き換えて」とだけ頼むと、sleepをwaitForTimeoutに写しただけのテストや、CSSセレクタをそのまま引き継いだテストができます。規則を先に置くと、1本目から方針が揃います。次はCLAUDE.mdに置く断片の一例です。
## SeleniumからPlaywrightへの移行規則
- sleep、暗黙的wait、WebDriverWaitは移さない。待機はPlaywrightの自動待機とexpectに任せる
- `waitForTimeout`は使わない。待ちたい条件を `await expect(...)` で書く
- ロケータはgetByRole、getByLabel、getByTextを優先する。IDやCSSは最後の手段
- `first()` `nth()` で複数ヒットを黙らせない。絞り込みは `filter()` で行う
- 旧テストのassertion1つにつき、新テストにexpectを1つ以上対応させる
- 移行した旧テストごとに、対応表 (旧assertion → 新expect) を migration-map.md に追記するこの規則は、公式のベストプラクティスにある「利用者から見える属性を優先する」「自動リトライするassertionを使う」に沿っています。最後の2行は移行の検証用に足した運用ルールで、Playwrightの推奨ではありません。次の節で使います。
暗黙的waitと明示的waitをどう置き換えるか
旧テストを3パターンに分けて見ると、置き換えが機械的になります。以下は考え方を示す例で、実際のコードは対象プロジェクトに合わせて変わります。
// 移行前 (selenium-webdriver)
await driver.manage().setTimeouts({ implicit: 10000 });
await driver.get(`${baseUrl}/login`);
await driver.findElement(By.css('#user')).sendKeys('demo');
await driver.findElement(By.css('.btn.btn-primary')).click();
const flash = await driver.wait(until.elementLocated(By.css('.flash')), 5000);
assert.equal(await flash.getText(), 'ようこそ');// 移行後 (Playwright Test)
await page.goto('/login');
await page.getByLabel('ユーザー名').fill('demo');
await page.getByRole('button', { name: 'ログイン' }).click();
await expect(page.getByRole('status')).toHaveText('ようこそ');置き換えのポイントは3つです。
- 暗黙的waitの設定行は丸ごと不要になる。セッション全体の待ち時間という概念が、操作ごとの自動待機に変わる
driver.wait(until...)と、その後のgetText比較は、expect(...).toHaveTextの1行にまとまる。expectは条件を満たすまで再取得して確かめ、既定の待ち時間は5秒.btn.btn-primaryのようなクラス依存のCSSは、ボタンの役割と名前に置き換える
移行前のimplicit: 10000や5000の待ちに合わせて、長く待ちたい画面が残ることもあります。その場合もwaitForTimeoutには戻さず、待ち時間だけを広げます。expectごとにawait expect(locator).toHaveText('ようこそ', { timeout: 10000 })と渡すか、全体で足りないならconfigのexpect.timeoutを変えます。前者は該当の画面だけ、後者はすべてのexpectに効きます。
Seleniumの公式は暗黙的waitと明示的waitを混ぜないよう警告しています。両方使っている旧テストは待ち時間が読めないので、移行前の時点では「どちらの待機で通っていたか」を決めにくいことがあります。その場合も、Playwrightへ移す際は待機を一本化できるので、旧テストの待ち時間設定は気にせず条件だけを読み取らせます。
非同期で値を取り出してから比べる書き方に注意
Playwrightにはexpect(await locator.isVisible()).toBe(true)のように、awaitしてからexpectに渡せてしまう書き方があります。公式はこれを手動のassertionとして戒めています。この形は待たずにその瞬間の値を返すだけなので、移行したテストが「出る前に調べて落ちる」か、逆に「何も待たずに通る」不安定な状態になります。SeleniumのgetText()を比べる癖が残ったまま移すと入り込みやすい形です。CLAUDE.mdの規則にも「expect(await ...)の形を使わない」を1行足しておくと、レビューで拾えます。
Playwright MCPで実画面を見てロケータを決める
ロケータを推測で書かせると、存在しないラベル名を使います。Playwright MCPはページをアクセシビリティのスナップショットとして返し、要素に参照番号を付けて操作します。スクリーンショットや座標に頼らない作りです。この仕組みを使うと、Claude Codeは実画面の名前を見たうえでロケータを書けます。
導入は1行です(詳細は導入記事を参照)。
claude mcp add playwright -- npx @playwright/mcp@latestチームで共有するなら--scope projectを付けると、.mcp.jsonにプロジェクト単位で保存されます。コミットすれば全員が同じMCPツールを使えます。移行作業中だけ使うなら、個人の設定のままで足ります。
依頼の仕方は、旧テストを渡し、画面の確認を挟ませる順にします。次のような指示を一例として使えます。
tests/legacy/login.test.js をPlaywright Testに移して。
移す前に、Playwright MCPでステージング環境のログイン画面を開き、
スナップショットに出ているボタン名とラベル名を使ってロケータを書くこと。
CLAUDE.mdの移行規則に従い、対応表をmigration-map.mdに追記して。この流れで効くのは、ロケータが実際の画面と食い違ったときです。MCPで見たラベルが「ユーザー名」なのに旧テストが#userだった、という差分がその場で出ます。実装側にアクセシブルな名前がない要素は、テストの書き方ではなくマークアップに手を入れるか、getByTestIdのような明示的な契約を使う判断になります。
移行前後で同じ検証をしているかを突き合わせる
ここが本記事の中心です。通るテストが書けたことと、同じ検証ができていることは別です。次の3段で確かめます。
- 件数の突き合わせ。旧テストの
assertやexpectの数と、新テストのexpectの数を並べ、減っていれば理由を言わせる。対応表のmigration-map.mdは、旧assertionの行番号と新expectの行番号を1対1で書く形にする - 待機が消えて減った検証がないか確認する。旧テストの
until.elementLocatedは「要素が出ること」自体の検証を兼ねていたことがある。新テストではtoBeVisibleなどを明示的に足す必要がある - わざと壊して落ちることを確かめる。アプリの表示文言を一時的に変える、期待値を1文字変えるなどして、新テストが赤になることを見る。これはPlaywrightの機能ではなく移行の検証用に足した確認方法で、通っているのに何も見ていないテストを見つけるためのものです
1つ目の件数は機械的に数えられます。次はあくまで目安を出すための粗い集計です。
grep -cE "assert|expect" tests/legacy/login.test.js
grep -cE "expect\(" tests/e2e/login.spec.ts数が合わないこと自体は誤りではありません。旧テストが1行で複数の確認をしていた、あるいは逆の事情もあります。差が出たら、その理由を対応表に1行書かせるのが目的です。3つ目のわざと壊す確認は、Claude Codeに実行させて赤になるログを読ませると、手作業が減ります。
実行環境:Selenium Gridを使い続けたい場合
全部を一度に移せないとき、PlaywrightをSelenium Grid上で動かす実験的な機能があります。環境変数SELENIUM_REMOTE_URLにHubのURLを指定し、npx playwright testを実行します。コードの変更は要りません。ただし制約が多く、次の点を押さえておく必要があります。
- 対応はGoogle ChromeとMicrosoft Edgeだけ
- 公式が「experimental」と明記し、将来壊れる可能性を警告している。内部ではChrome DevTools Protocolのwebsocketで接続し、Selenium 4がこの機能を出している間だけ動く
- Selenium 3は、グリッドのノードへ直接アクセスできる場合にだけ動く、ベストエフォート扱い
認証ヘッダーや追加のcapabilityが必要なグリッドでは、SELENIUM_REMOTE_HEADERSとSELENIUM_REMOTE_CAPABILITIESにJSONを渡します。接続の様子はDEBUG=pw:browser*で見られます。つまり、Gridで当面しのぐ移行は可能ですが、最終的な到達点としては勧められにくい構成です。段階移行の途中の足場として使う構成です。
よくあるつまずき
複数ヒットでstrict違反になる
旧テストがfindElements(...)[0]のように先頭を取っていた箇所で起きます。first()で黙らせる前に、filter({ hasText })や親要素の絞り込みで一意にできないかを試します。「一覧の2件目」のように順番が本質的な意味を持つ場合はnth()が妥当ですが、その理由を対応表に残すと後で追えます。
sleepをwaitForTimeoutに写してしまう
指示に待機の置き換え規則がないと起きます。waitForTimeoutは固定時間の待ちで、公式がSeleniumのsleepについて挙げる弱点(短すぎれば失敗、長すぎれば遅い)がそのまま当てはまります。CLAUDE.mdに「使わない」と書き、条件をexpectで表現させます。
「出ないこと」の確認を肯定形のexpectで書いてしまう
要素が出ないことを確かめたいときは、toBeHiddenかtoHaveCount(0)のように、確かめたい状態に合うexpectを選びます。公式のexpect一覧にはどちらも自動リトライする形で載っています。旧テストで例外の捕捉やsleepで表していた「消えるまで待つ」は、これで書き換えられます。
まとめ
Seleniumの待機コードは、移行では写すのではなく消します。残す価値があるのは、その待機が兼ねていた「要素が出ること」という検証だけです。ロケータはPlaywright MCPで実画面の名前を見て決め、移行規則と対応表をCLAUDE.mdとmigration-map.mdに置けば、Claude Codeの出力が揃います。最後にわざと壊して赤になることを見るまでが、一本の移行の終わりです。
Playwrightを使った継続的な自動実行はhookでE2Eを自動実行する手順に続けられます。モバイルのテストを移す場合は、検証範囲の線引きをAppiumのE2Eテストで見比べられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。