ノーコードE2EからPlaywrightへ移行して変わった、E2Eの考え方
自己紹介
SkyfallでQAチームのリーダーをしている、imaiです。
普段は、品質管理責任者として品質改善やE2Eの整備に携わりながら、チームマネジメントも担当しています。
背景
もともと、Datadog Synthetic Browser Testing を使って、ブラウザベースのテストを運用していました。ノーコードで始めやすいことが大きなメリットで、導入のハードルも低かったからです。
一方で、運用を続ける中で少しずつ以下のような課題も見えてきました。
- テストの安定性
- 修正やメンテナンスのしづらさ
- テスト改善の自由度
また、当時はE2Eを実装できるメンバーが限られており、
「テストを書く」「壊れたテストを直す」「より良い形に改善する」といった営みが、特定の人に寄りやすい状態でもありました。
その結果、テストがある程度は動いていても、
- なぜそのテストになっているのか
- 何を担保したいのか
といった判断がしづらく、運用や改善の幅に限界がありました。
そうした背景から、Playwright への移行を検討するようになりました。
なぜPlaywrightに移行したのか
移行理由はいくつかありますが、特に大きかったのは次の点です。
ノーコードツールの修正コストの高さ
ノーコードで始めやすいのは確かにメリットでしたが、
運用フェーズに入ると、ちょっとしたUIの変更でも影響を受けやすく、修正コストの高さが目立つようになりました。
ノーコードツールでは、文言変更など軽微なUIの修正のたびに、画面上で手作業で修正していく必要がありました。
コードであれば差分としてレビューできる点も、ノーコードツールでは変更の意図やエラー箇所の特定が追いづらく、地味な修正コストが積み重なっていきました。
Datadog でできていたことをある程度カバーできた
もちろん、Datadog と Playwright はまったく同じ役割ではありません。
ただ、私たちが日常的に担保したかった主要な観点は、Playwright で十分に置き換え可能でした。
- ユーザー視点に沿った操作フローのテスト
- 自動待機による安定したアサーション
- CI との連携
特に Playwright の自動待機は優秀で、waitForTimeoutなどの明示的な時間ベースの待機をほぼ書かなくて済むようになりました。
ユーザー視点に沿ったテストを書きたかった
Datadog は操作を記録して再生するスタイルのため、「ユーザーが何をしようとしているか」より「どの要素をクリックしたか」に寄りがちでした。
一方 Playwright では、ユーザーが実際に認識している要素に基づいたテストを書くことができます。
「ID欄に入力する」「ログインボタンを押す」といった操作の意図がコードに表れるため、テストを読むだけでユーザーの行動フローが伝わるようになりました。
移行して変わったこと
セレクタの設計を意識するようになった
以前より「壊れにくいテストを書くにはどうセレクタを選ぶか」を意識するようになりました。
Playwright には Best Practices が公式ドキュメントにまとめられており、「ユーザーが実際に見ているもの」に基づいたセレクタを推奨しています。
data-testidやgetByRole、getByLabelなどを使うことで、実装の変更に強いテストが書けるようになりました。
E2Eで何を担保するべきかを考えるようになった
「このテスト、何を確認したいんだっけ?」と自問する機会が増えました。
ノーコードツールでは「操作を記録してそのまま再生」という感覚になりがちですが、コードで書くと意図が明示されるため、テストの目的を考える習慣がつきました。
維持しやすいテストを書く意識が強くなった
「なんとなく動くテスト」ではなく、「壊れたときに原因がわかるテスト」「直しやすいテスト」を意識するようになりました。
テストが壊れたときに修正できるメンバーが増えたことも、大きな変化でした。
こうした変化を通じて、実際にE2Eを書くときに意識するポイントも少しずつ整理されてきました。
E2Eを書くときに意識していること
1. 待機は「何を待ちたいか」を意識する
Playwright には自動待機の仕組みがあり、それを活かしつつ「次の操作に入るためにどの状態を待つべきか」を意識して書くようにしています。
// 👎 時間で待つ
await page.waitForTimeout(2000);
// 👍 状態で待つ
await expect(page.getByRole('button', { name: '送信' })).toBeVisible();
時間待ちは「なんとなく動く」テストの温床になりやすく、CI環境によって不安定になることもあります。Playwright では expect().toBeVisible() のような Web-first assertion を使うことで、単に待つだけでなく、「表示されていること」の確認まで含めて書けるのも大きな利点です。
特にCIではローカルより描画や通信のタイミングがブレやすいため、「十分待ったはず」ではなく「次の操作が成立する状態になったか」で待つことを意識しています。
2. セレクタは実装都合に寄りすぎない
CSSクラスやDOMの階層構造に依存したセレクタは、UIの変更に弱く、テストが壊れやすくなります。できる限りユーザーが認識できる情報をもとにセレクタを書くようにしています。
// 👎 実装都合のセレクタ(壊れやすい)
await page.locator('.btn-primary > span.label').click();
await page.locator('#app > div:nth-child(2) > button').click();
// 👍 ユーザー視点のセレクタ
await page.getByRole('button', { name: '送信' }).click();
await page.getByLabel('メールアドレス').fill('test@example.com');
await page.getByTestId('submit-button').click(); // data-testid を付与している場合
getByRole や getByLabel は、ユーザーが画面上で認識する意味やラベルに沿って要素を取得できます。
まずはユーザーが認識できる情報をもとに要素を取得できないかを考えるようにしています。
一方で、どうしてもユーザー向けの属性だけでは安定して取得しづらい場合もあるため、そのような場面では data-testid を補助的に使います。
あくまでユーザーは data-testid を見て操作しているわけではないため、最初からそれに頼るのではなく、必要な場合に限って使う、という順番を意識しています。
3. 共通化しすぎない
「同じ操作が繰り返されているから共通化しよう」という発想は自然ですが、E2Eで過度に共通化すると、テストが読みづらくなったり、共通部分の変更が意図しないテストを壊す原因になります。
// 👎 過度に共通化(何をしているかわかりにくい)
await loginAndNavigate(page, '/dashboard');
await performStandardAction(page, 'submit');
// 👍 多少冗長でも、テストの流れが一目でわかる
await page.goto('/login');
await page.getByLabel('メールアドレス').fill('user@example.com');
await page.getByLabel('パスワード').fill('password');
await page.getByRole('button', { name: 'ログイン' }).click();
await expect(page).toHaveURL('/dashboard');
共通化する場合は、ログイン処理のように明確にまとまりがあるものにとどめて、テストの流れ自体が見えなくならないように意識しています。
E2Eでは再利用性だけでなく、「このテストで何を確認しているか」が読み取れることも重要だと感じています。
4. アサーションは「ユーザーが確認できること」で書く
操作の結果を確認するとき、内部状態ではなく、画面上でユーザーが確認できる変化をアサートするようにしています。
// 👎 実装に依存したアサーション
const result = await page.evaluate(() => window.__store__.state.submitted);
expect(result).toBe(true);
// 👍 ユーザーが見える変化をアサート
await expect(page.getByText('送信が完了しました')).toBeVisible();
await expect(page.getByRole('alert')).toContainText('エラーが発生しました');
これによって「このテストが何を確認しているか」がコードを読むだけで伝わりやすくなり、実装変更の影響も受けにくくなります。
画面遷移のテストでは、URL検証だけでなく、遷移先でその画面らしい要素が表示されていることまで合わせて見るようにしています。
5. E2Eに過剰な責務を持たせない
E2Eを書くときは、「その確認は本当にE2Eでやるべきか」を意識するようにしています。
画面上の主要な導線や、ユーザーが実際にたどるフローを担保するのがE2Eの役割であり、細かなロジック確認や内部実装に寄った検証まで持ち込むと、E2Eの責務が広がりすぎてしまいます。
結果として、実行が重く、壊れやすく、直しにくいテストになりやすいため、E2Eで何を担保し、何を別のレイヤーに任せるかを意識しています。
複雑な計算ロジックや細かな条件分岐までE2Eで確認しようとすると、セットアップが重くなり、失敗したときの切り分けもかなり難しくなります。
そのため、そうした確認はユニットテストなどの、より適したテストレイヤーに任せ、E2Eでは主要なユーザーフローの担保に集中するようにしています。
まとめ
- Datadog から Playwright に移行したことで、E2Eの書き方だけでなく考え方も見直せました
- 「なんとなく動くテスト」より「維持しやすく、意図が伝わるテスト」を書く意識が大切だと感じました
- セレクタ設計・待機戦略・共通化の粒度、どれも「テストが何を伝えたいか」に立ち返ることで判断しやすくなりました
移行当初は、コード量が増えることに不安もありましたが、今では「テストを読めば、その操作フローや確認したいことが伝わる」状態に少しずつ近づいてきています。
また、特定の人しか触れないものではなく、チーム全体でテストに関われる土台も作りやすくなりました。
Playwrightへの移行は、単なるツール変更ではなく、E2Eの役割や書き方を見直すきっかけにもなりました。
これからE2Eの導入や見直しを考えている方の参考になればうれしいです。
Discussion