Seleniumのテストが増えてきたら一度は考える話
Seleniumで書いたテストコードがそこそこの本数になってきて、そろそろPlaywrightに移行すべきかという相談を最近よく受けます。私自身、長年RubyとSelenium WebDriverでテストを書いてきました。ここ数年はTypeScriptとPlaywrightがメインです。正直、最近はSeleniumもRubyもほとんど書いていません。
ただ、移行の相談を受けるたびに感じるのは「移行そのものが目的化している」ケースがそこそこあるということです。今回はまず移行すべきかどうかの判断基準から入って、実際に手を動かすためのAPI対応表、待機・セレクタ・ウィンドウ操作の書き換えパターン、最後に段階的な移行の進め方までまとめます。
そもそも移行すべきかどうかの判断基準
結論から先に言うと、今動いているSeleniumのテストが安定していて誰も困っていないなら、無理に移行しなくていいと思っています。移行にはそれなりの工数がかかりますし、チームが今のコードを読めなくなるリスクもあります。
私の感覚では、次のような場面では移行を検討する価値があります。
- waitやsleepの調整に時間を取られていて、CIがよく落ちる
- 新しく入るメンバーがPlaywright経験者ばかりで、Selenium側の保守が属人化している
- 並列実行やトレース(実行ログの記録)を今より強化したい
- ブラウザを複数タブ・複数コンテキストで同時に操作するテストが増えてきた
逆に、次のような場合は今すぐ移行しなくていいと考えています。
- テストが少数で、今のところ安定して回っている
- チームがRubyやPythonでのSelenium運用に慣れていて、書き換えの学習コストの方が大きそうな場合
- 「流行っているから」以外に明確な理由がない場合
私自身がPlaywrightに乗り換えたのも、待機処理の不安定さに疲れたのが一番の理由でした。逆に言えば、そこで困っていないなら急ぐ必要はありません。移行しないという判断も、立派な選択肢です。
API対応表(早見表)
移行すると決めたら、まずはよく使う操作の対応関係を頭に入れておくと迷いにくいです。SeleniumはRuby、PlaywrightはTypeScriptの例で並べます。
| 操作 | Selenium (Ruby) | Playwright (TypeScript) |
|---|---|---|
| 要素取得 | driver.find_element(:css, '...') |
page.locator('...') / getByRole / getByLabel |
| クリック | .click |
.click() |
| 文字入力 | .send_keys('...') |
.fill('...') |
| テキスト取得 | .text |
.textContent() |
| 明示的待機 | Selenium::WebDriver::Wait.new(timeout: 10).until { ... } |
基本不要(auto-wait)。明示するならexpect(locator).toBeVisible() |
| スクリーンショット | driver.save_screenshot('x.png') |
page.screenshot({ path: 'x.png' }) |
| 別ウィンドウ/タブ | window_handles / switch_to.window |
context.waitForEvent('page') |
| iframe内操作 | switch_to.frame / switch_to.default_content |
page.frameLocator('...') |
| アラート処理 | switch_to.alert |
page.on('dialog', ...) |
| 終了処理 | driver.quit |
テストごとにcontextが自動で片付く |
表だけ見ても実感が湧きにくいと思うので、ここからは実際によく詰まるところをコードで見ていきます。
書き換えパターン1:待機処理
Seleniumでよく見る待機処理はこんな形です。
wait = Selenium::WebDriver::Wait.new(timeout: 10)
wait.until { driver.find_element(:css, '.welcome-message').displayed? }
要素が見つかるかどうかをループで確認し続ける、いわゆる明示的待機です。sleepよりはずっとましですが、timeoutの秒数調整や、要素は見つかったのに中身のテキストがまだ描画されていないといった細かい不安定さに悩まされた人も多いと思います。
Playwrightではこの手の待機がほぼ不要になります。
await expect(page.getByText('ようこそ')).toBeVisible();
locatorベースのアサーションは、条件を満たすまで自動でリトライしてくれます。要素が現れてタイムアウト内にアサーションが通れば成功です。timeoutの秒数を毎回考えなくていいので、これだけでもコード量がかなり減ります。ここからが本番です。待機を明示的に書かなくていいという前提に慣れると、他の書き換えパターンも素直に読めるようになります。
書き換えパターン2:セレクタの選び方
Seleniumのコードでは、CSSセレクタやXPathで要素を指定することが多かったと思います。
driver.find_element(:css, '#login-form > div:nth-child(2) > button').click
この手のセレクタは、デザイナーがクラス名を整理しただけでテストが軒並み落ちる原因になります。CSSセレクタでテストを書いていれば誰もが一度は経験することだと思います。
Playwrightでは、画面の見た目や役割をもとにしたロケーターが推奨されています。
await page.getByRole('button', { name: 'ログイン' }).click();
getByRoleはクラス名ではなく、ブラウザが認識している「役割と名前」を見て要素を探します。マークアップの構造をいじってもテストが壊れにくくなるので、移行のついでにセレクタも作り直すと効果を実感しやすいです。Rubyのコードから丸ごと移行した経緯は以前Selenium(Ruby)からPlaywright(TypeScript)へ移行して変わった考え方にまとめたので、あわせて読んでみてください。
書き換えパターン3:ウィンドウ・タブ・iframe操作
別タブが開くリンクのテストは、Seleniumだとwindow_handleの差分を取って切り替える書き方が定番でした。
original_window = driver.window_handle
driver.find_element(:css, 'a[target="_blank"]').click
wait.until { driver.window_handles.size > 1 }
new_window = (driver.window_handles - [original_window]).first
driver.switch_to.window(new_window)
driver.find_element(:css, 'h1').text
driver.close
driver.switch_to.window(original_window)
Playwrightだと、新しいページが開くイベントを直接待てます。
const [newPage] = await Promise.all([
context.waitForEvent('page'),
page.getByRole('link', { name: '詳細を見る' }).click(),
]);
await newPage.waitForLoadState();
await expect(newPage.getByRole('heading')).toBeVisible();
await newPage.close();
window_handlesの差分を自分で計算する必要がなくなるので、コードの見通しがだいぶよくなります。これ、意外と使えます。
iframe内の操作も同様です。Seleniumはフレームに切り替えてから戻る必要がありました。
driver.switch_to.frame(driver.find_element(:css, 'iframe#payment'))
driver.find_element(:css, '#card-number').send_keys('4242424242424242')
driver.switch_to.default_content
PlaywrightではframeLocatorがフレームの中身をそのまま扱わせてくれます。
const paymentFrame = page.frameLocator('#payment');
await paymentFrame.locator('#card-number').fill('4242424242424242');
切り替えて戻すという手順そのものがなくなるので、書き忘れによる「フレームの外に戻し忘れて次のテストが失敗する」という事故もなくなります(決済フォームのようにiframeが入れ子になっているケースまでは検証しきれていないので、複雑な構造の場合は手元で挙動を確認してください)。
演習:手元のテストを1本書き換えてみる
ここまでの対応表とパターンを使って、手元にあるSeleniumのテストを1本選んで、Playwrightに書き換えてみてください。ヒント:いきなり全部やろうとせず、待機処理から手をつけると迷いにくいです。待機を減らせると、その次のセレクタの書き換えも自然と進みます。
段階的な移行の進め方
一気に全部書き換えるのはおすすめしません。テスト資産が大きいほど、途中で息切れします。私が実際にやっている進め方は次の順番です。
- 新規に追加するテストは、理由がない限り全部Playwrightで書く
- 既存のSeleniumテストのうち、CIでよく落ちる不安定なものから優先的に移行する
- 移行が終わるまでの間はSeleniumとPlaywrightのテストをCIで両方走らせる(実行時間は伸びますが、これは移行期間の必要経費だと割り切っています)
- Selenium側のテスト本数が十分減った段階で、完全移行のタイミングをチームで決める
というわけで、移行は「えいやで全部書き換える」ものではありません。「新規はPlaywright、既存は困っているものから」で少しずつ進めるのが現実的だと思っています。途中で「やっぱりSeleniumのままでいい」と判断を戻すのもありです。移行はゴールではなく手段なので、途中で困りごとが解消したならそこで止めてしまって構いません。
まとめ
SeleniumからPlaywrightへの移行は、待機処理・セレクタ・ウィンドウ操作の3つの書き換えパターンさえ押さえれば、思っているより早く進みます。ただし移行そのものが目的にならないよう、まずは今困っていることがあるかどうかを確認してから着手することをおすすめします。API対応表は保存版として、実際に書き換えるときに見返してもらえればと思います。詰まったところがあれば、コメントで教えてください。
Playwrightの待機処理やアサーションの選び方をもう少し深掘りしたい方は、Playwrightのexpectマッチャー完全リファレンスもあわせてどうぞ。AIにテストを書かせる方法に興味がある方はPlaywright MCP入門から読んでみてください。


コメント