Claude Media
Claude CodeでSeleniumのテストを移行する — Playwrightの待機とロケータに置き換え

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@latest

TypeScriptか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で失敗します。つまり、旧テストにある待機コードの多くは「書き換える」のではなく「消す」対象です。

観点SeleniumPlaywright
待機の置き場所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段で確かめます。

  1. 件数の突き合わせ。旧テストのassertやexpectの数と、新テストのexpectの数を並べ、減っていれば理由を言わせる。対応表のmigration-map.mdは、旧assertionの行番号と新expectの行番号を1対1で書く形にする
  2. 待機が消えて減った検証がないか確認する。旧テストのuntil.elementLocatedは「要素が出ること」自体の検証を兼ねていたことがある。新テストではtoBeVisibleなどを明示的に足す必要がある
  3. わざと壊して落ちることを確かめる。アプリの表示文言を一時的に変える、期待値を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テストで見比べられます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. playwright.dev/docs/actionability
  2. playwright.dev/docs/locators
  3. playwright.dev/docs/best-practices
  4. playwright.dev/docs/selenium-grid
  5. selenium.dev/documentation/webdriver/waits
  6. code.claude.com/docs/en/mcp
  7. playwright.dev/docs/test-assertions
  8. playwright.dev/docs/test-configuration
  9. playwright.dev/docs/intro
この記事を共有:XはてブLinkedIn