🚄

E2EをノーコードツールからPlaywrightに移行した

に公開

はじめに

今回は弊社のTANOMUというプロダクトのE2Eテストを、ノーコードのGUIベースのE2Eツールから Playwright へ移行した話をします。

この移行によってテストの実行時間が約60分から約12分へ短縮されたり、コスト的なメリット、運用の改善などメリットは多々あったのですが、それなりに大変だったのでノウハウの共有をします。

元々使ってたツールがよしなにやってくれていた部分がPlaywrightに移行することで、爆速かつ厳密な挙動へ変わったことで、「テストが速すぎて落ちる」ことへの対処がいろいろ必要だったので、今回は主にその部分のノウハウ共有になります。

なぜ移行したのか

もともとは数年前にE2Eの必要性が出てきたタイミングで、いくつかツールを比較したうえで、ノーコードツール(SaaS)を選定しました。
その後、長く増改築を繰り返したり運用するなかで、いくつかの課題がでてきました。

主には「修正コストの高さ」と「レビューの難しさ」を感じました。

アプリケーション側に広範囲DOMの変更などが入った際、壊れたシナリオを一つひとつ画面上でポチポチと直していく作業が発生します。
これがかなりしんどくて、影響範囲をgrepできないし、適切な共通化も難しかったので、とにかく気合で修正していく、しかありませんでした。

変更前後の差分が確認できるわけでもないので、「何をどう修正したのか」という部分のレビューもできず、1つ1つの修正や追加したテストが適切なのかどうか、というレビューをするのも現実的ではありませんでした。
結果として、E2E職人による職人芸で運用するという、あんまり良くない世界観になってしまいました。

フェーズの変化

誤解のないように補足しておくと、以前のツールを採用したこと自体は当時の判断としては正しかったと思っています。
「低コストで素早くE2Eを構築する」というのが目的だったので、当時のフェーズではGUIベースのツールが最適解でした。実際に、そのおかげで品質を担保できましたし、構築コストもあまりかけずにそれを実現できていました。

しかし、プロダクトの規模が大きくなり、開発スピードも上がった現在には則してないと判断し、今回より堅牢で、エンジニアがGitベースで管理・レビューでき、メンテナンスしやすいPlaywrightに移行することにしました。

なぜPlaywrightか

移行先として Playwright を選定した理由は主に3つあります。

1. デファクトスタンダードであること

E2E界隈の変遷について詳しいわけではないのですが、古くはSeleniumやTestCafeなど様々な技術やフレームワークが登場し、現代ではPlaywrightがデファクトスタンダードになってるのかなという印象です。
特に突飛なE2Eをやりたいわけでもなかったので、実績もドキュメントも豊富なPlaywrightを選定することにしました。

2. マネージドな実行環境があったこと

もう一つタイミングが良かったのが Microsoft Playwright Testing が出てきたことです。
自前で実行環境を準備したくなかったので、スケーラブルな実行環境を課金するだけで用意できるのは魅力的でした。

現在はMicrosoft Playwright Testingはたしかdeprecatedで、Playwright Workspace というサービスに変わってしまいましたが、基本的な部分では変化がないので特に問題ないです。

Playwright Testeingは実行結果の確認画面を用意してくれてたのに、 Playwright Workspace は実行結果をDLしてローカルで見るしかなくて、なんかそこだけサービスレベルが低下してるのがちょっとやだな、というくらいです。

3. 社内QAエンジニアのスキルセット

地味に大きな理由として、社内のQAエンジニアが既にPlaywrightでコードを書けるスキルを持っていました。

エンジニアは覚えりゃ書けるだろう、という温度感は元からあったのですが、E2EのメンテナンスはQAエンジニアにメインでお願いしていたこともあって、チーム内のQAエンジニア全員がPlaywrightを書ける状態だったのは僥倖でした。

コードを書くことに抵抗がないQAエンジニアがいてくれたからこそ移行を決意できたなと思ってます。

ところがPlaywrightは「速すぎる」

いざPlaywrightへの移行を始めて最初にぶつかったのがPlaywrightが「速すぎる」ことでした。

ノーコードツールで問題なく動いていたシナリオをそのままPlaywrightで実装しても、テストが落ちまくる感じで、困ったなぁと。

よくあったのが、以下のような単純な画面遷移を含むフローです。

  1. 一覧画面で「詳細へ」ボタンをクリックする
  2. 詳細画面へ遷移する
  3. 詳細画面にある「編集」ボタンをクリックする

以前のツールであれば何の問題もないこのフローですが、Playwrightでは 「3. 遷移後の画面のボタンを押す」のタイミングでテストが落ちるケースが頻発しました。

エラーの内容は「要素が見つからない」「クリック可能な状態ではない」といったものですが、スクリーンショットを見ると画面は表示されているように見えるという状況です。

落ちる原因

テスト結果を分析してみると、

  • 旧ツール
    • 動作が良い意味でゆっくりでした。
    • 1アクションごとに数秒のタイムラグがあったり、「画面の描画が落ち着くまで待つ」といった処理がツール側で暗黙的に行われていました。
    • その「遅さ」がバッファとなり、裏側の処理完了を自然と待てていたようです。
  • Playwright
    • とにかく爆速です。
    • await page.click() が完了した次の瞬間には、ミリ秒単位の隙もなく次の行を実行しようとします。

この結果、以下のようなステップが必要なことがわかってきました。

  1. 一覧画面で「詳細へ」ボタンをクリックする
  2. 詳細画面へ遷移する
  3. (NEW!) 遷移先でJSが実行され、データをフェッチするなどの初期化が完了するのを待つ
  4. 詳細画面にある「編集」ボタンをクリックする

なので、とにかくいろんな箇所で「いい感じに待つ」という処理を実装する必要がありました。

「いい感じに待つ」方法

実行しては修正する、を繰り返す、泥臭い作業が必要だったんですが、その中でも頻出するテクニック?があったのでいくつか紹介します。

Tips1: waitForResponse() の活用

弊社のアプリケーションはSPAのWebアプリケーションです。
なので、画面遷移時に初期化処理として、その画面に必要なデータをAPIリクエストで取得してきます。

一覧画面なら一覧画面用のAPIを叩き、詳細画面なら詳細画面用のAPIを叩く、という感じです。

多くのケースで、この「初期化処理中に実行されるAPIのレスポンスを待つ」ことで「いい感じに待つ」を実現できることが多かったです。

たとえば詳細ページから一覧ページに戻ってくるようなケースでは、

// 詳細画面のボタンを押す
await page.locator('...').click()
// 一覧ページに戻ってきて、初期化処理が終わるのを待つ
await page.waitForResponse(/\api\/v1\/foos\?page=1/)

のようにその一覧ページで叩かれるAPIリクエストのレスポンスを待ちます。

アクションとwaitForResponseはPromise.allで宣言すること

ただ、実際には上記だと不十分で、「クリックした後、waitForResponse する前にAPIレスポンスが返ってくる」というケースが発生します。
APIレスポンスが早すぎて発生しちゃう的な感じです。
そうすると「レスポンスが帰ってきたあとにwaitForResponse()してるから、延々と待ち続けてコケる」ということが発生します。

なので、基本的には以下のように、期待する動作のセットを Promise.all でwrapすることでテストを安定させることができます。

await Promise.all([
  page.locator('...').click(),
  page.waitForResponse(/\api\/v1\/foos\?page=1/)
])

この場合は、click と waitForResponse が(ほぼ)同時に実施されるので、「レスポンスが返ってきたあとに waitForResponse しちゃう」ということが発生しません。

Tips2: expect().toBeVisible() / expect().toBeHidden() の活用

「いい感じに待つ」際に、APIリクエストが発生するとは限らないので、DOMの出現、あるいはDOMが隠れるのを待つこともよくあります。

await Promise.all([
    page.locator('...').click(),
    expect(page.locator('...')).toBeVisible()
])

こちらも waitForResponse と同様に、 アクションと同時に実行するほうが無難かなと思っています。

Tips3: Xpathの活用

PlaywrightにおいてDOMを特定する方法はいくつかありますが、最終的にXpath覚えるのが一番楽だなとなりました。

書き方は冗長なので好き嫌いは分かれるところなのは承知していますが、以下のメリットを感じています。

  • contains(., '文字列') で「テキストを含む」が表現できる
  • following-siblingparent などで階層を行ったり来たりできる
  • consoleで $x("//div[contains(@class, 'foo')]") を叩くだけであってるか確認できる

冗長ですが表現力は高く、必要なDOMをペペッと取得できる点を気に入ってます。

もちろんPlaywrightでは getByRole()filter({ hasText: '...' }) が推奨されているのは把握しているんですが、実際の画面をChromeで触りながら指定があってるか確認できるxpathは相当にお手軽で、強力だなという印象です。

最終的に得られた成果

頑張って全部の「いい感じに待つ」を調整したり、その他並列実行に強くなるように様々な調整をした結果、worker5つで安定して動作するようになり、もともと60分以上かかっていたテストが12分程度で完了するようになりました。

ほんとはworkerを20個くらい並列で動かして爆速でE2Eを終わらせるようにしたかったのですが、テスト対象の環境(サーバー)は貧弱なものを利用しているので、そこまでの高速化は諦めました。

  • いい感じに待てて無くてコケる問題
  • 並列実行することで初めて発生するトラブル
    • 例えば「リストの一番上の要素」という指定をしていたが、並列実行するとリストが他のテストによって更新されるので破綻する、など
  • 並列数が多くなるとサーバーの負荷が高くなってレスポンスを返せなくなる問題

これらをひとつずつ潰していくことで、ひとまずメンテナンスでき、安定してテストが実行できるような状況を手に入れることができました。

当初想定よりは手がかかってしまいましたが、結果的に良い状況が得られたのでやってよかったです。

補足: Playwrightのディレクトリ構造やデザインパターン

どういうディレクトリ構造で作るべきか、というのがよく分からなかったので、以下のようなディレクトリ構造で作ってます。
手探りなので良いのか悪いのかはわかってないです。

src/
  facades/
    SomethingActionFacade.ts
  fixtures/
    CreateSomethingFixture.ts
  pages/
    auth/
      LoginPage.ts
    foo/
      FooListPage.ts
    bar/
      BarListPage.ts
  utilities/
    dateTimeUtilities.ts
    somethingUtilities.ts
tests/
  foo/
    foo_list.spec.ts
  bar/
    bar_list.spec.ts
  • src/facades
    • 複数のページにまたがる一連の操作を共通化する処理をFacadeと命名しました
    • Facadeパターンというのがあるらしいので、雰囲気だけ乗っかりました
  • src/fixtures
    • Playwrightの Fixture を使うことで、事前にデータを定義できるので、それらを格納しています。
  • src/pages
    • PageObjectModel で、1ページに限定した処理を表現するclassを定義します
  • src/utilities
    • ランダムな文字列を生成するとか、日付計算とか、ユーティリティ類を格納します
  • tests
    • 各テストケースを実装しています

これで良いのか悪いのか、何も自信はもてないのですが、ひとまずこれで破綻なく運用できています。

TANOMU

Discussion