ブラウザテストを導入する上で考えるべきこと

この記事は何

実は株式会社 HERP で業務委託として Web エンジニアのお仕事をしているんですが、その中でブラウザテスト(E2Eテスト)を導入する業務に携わりました。*1 業務の中で、Autify などのサービスを利用せずに自前(Puppeteer + Mocha)でブラウザテストを構築しました。

実装やら設計を進める中で学びがあったので、ブログという形でブラウザテストの導入において考えておくべきこと、注意しておくべきことをまとめようと思います。 以下の内容は完全に主観と経験に基づくもので、絶対的に良いものでもなければ、ケースによってはアンチパターンを踏み抜いている可能性もあります。 こういった考え方もあるんだな程度に考えてくださると幸いです。

本編

前提

まず、テストの対象となっているアプリケーションは以下のような性質のものでした。

  • toB の Web サービス(4年選手ぐらい)
  • フロントエンドにもそれなりに複雑なロジックや処理が乗っているデータ管理系のアプリ
  • 公式な推奨環境は新しめの Chrome のみ
  • ユニットテストはかなり整っている。

上記のような状態で、アプリケーションの治安としてはかなり良い方だったのではと思います。 ただ一方で、大きなデプロイ前は人手で多くのテストケースの手動テストが必要であったり、よくデプロイ後に主要な機能が動かなくなってしまうなどの課題があり、それらを解決する方法としてブラウザテストを導入したいという話が上がりました。

考えるべきこと

ブラウザテストを導入すると言うのは簡単ですが実は意外と考えるべきことは多いです。 考えるべきこと、というよりは決めておくべきこと、要するに設計が必要です。

ここで言う設計はブラウザテストのコードアーキテクチャに留まらず、テストの設計であったり、ワークフローなども含まれます。

ざっと項目だけ洗い出すと、導入にあたっては以下のような観点について考えるべきだと思います。

  • 何をテストするか(逆に言えば、何をテストしないか)。
  • テストをいつどこで誰が回すか。
  • テスト結果はどのような形式で、どこで閲覧できるか。
  • ブラウザテストのアーキテクチャ・設計をどうするか。

こういった項目の必要性は、 JSTQB のシラバス にも記述があります。

以下、それぞれもう少しだけ具体的に見ていきましょう。

何をテストするか

これは逆に言えば何をテストしないかです。

ブラウザテストは一般にコストが大きいと言われています。 これは、実装が大変であったり、テスト自体が不安定であったり、テストにかかる時間が多かったりといったことを指しています。

そのため、ブラウザテストで何をテストするかを決めることは大切です。 たとえば、色々な入力条件に応じて正常系・異常系が切り替わる場合に、全てを網羅的にブラウザテストで確認するのは現実的ではありません。

まず初めに、ブラウザテストで何をチェックしたいのかを決めると、自ずとテストケースが決まります。 要するにブラウザテストを導入する目的を明確にすることですね。

今回の僕のケースで言えば、「デプロイ前に主要機能について正常に動作するか確認したい」というのが要求としてありました。 この目的を達成するのであれば、「主要機能の正常系について動作確認」が出来ればよいことになります。

まあ、不正な入力時にエラーバリデーションが走るかなどはチェックした方が良いケースもありますが、それは意外とユニットテストで網羅的にカバーできることでもあります。 不正な入力で変なデータが作られるより、正常な入力で正常なデータが作れない方がサービスとしてはまずいという優先度付けをここではしています。

個人的なオススメは、「あるリソースに対する CRUD 処理に対応する正常系のテストケースを1つずつ用意する」です。 たとえば、TODO リストであれば「タスクの作成、タスク詳細の表示、タスクの更新、タスクの削除」がそれぞれ正常に動くことが確認できれば、ブラウザテストの責務としては十分かなと思います。

以上が基本方針です。

それに加えて、どうしてもチェックしたい異常系や、手動で確認するのが大変な複数条件の組み合わせのテストなどを追加するのが良いのではないでしょうか。 コストとの兼ね合いですが、絶対に正常系以外をチェックすべきではないというポリシーではないので必要に応じてテストを増やすのが良いと思います。

テストをいつどこで誰が回すか

テストは回さなければ意味がありません。

まず、「いつ」についてですが、これは諸説あると思います。 まず真っ先に思いつくタイミングとしては、サービスの CI に組み込んでしまうというものがあるでしょう。

ただ、僕はあまりこれはお勧めしません。 先ほども言ったようにブラウザテストは時間がかかり、動作も不安定です。 ちょっとサービスのリクエストが詰まると失敗したり、CI にやたら時間がかかって開発体験が悪化すると思います。 もし CI に含めるとしても、optional なジョブとして定義するようにして開発のワークフロー自体は阻害しないような形で組み込むのをおすすめします。

個人的には、デプロイの前に開発者の何らかの操作によってブラウザテストを回す、というのが良いと思っています。 ブラウザテストの対象は、ローカルでも良いですし、production に出る前の staging 環境 *2 に対してやるでも良いです。 もし staging 環境があるのであれば、Slack 上やコンソール上からぽちぽちボタンを押すとブラウザテストが勝手に staging に対して回るようにしておくとかなり楽ですね。 僕のケースでは、CircleCI 上にブラウザテスト用のワークフローを用意して、 CircleCI のコンソール画面からワークフローを手動で起動してテストを回すような仕組みを用意していました。

このあたりは、何らかのワンショットバッチで実行する仕組みを整えても良いですし、もし余力があれば「ブラウザテストを回すアプリ」なんてのをデプロイしても良いかもしれません。 そこまで行くとまあ Autify 使えよって話になりそうですけど。

誰が回すかについては、基本的にはデプロイを実行する人か動作確認をする人が回すと良いと思います。 繰り返しになりますが、デプロイのフローの中に組み込むイメージですね。

テスト結果はどのような形式で、どこで閲覧できるか。

まずテスト結果の形式について。 ざっくり分けて2つのテスト結果を出力しておくと便利です。

1つはテキストのログ形式。 ここには、テスト名、テストの成否、入力した内容、テストの所要時間、失敗した場合はエラーメッセージ、などが含まれていると良いでしょう。

1つは動画。 これ、スクリーンショットだけでなく動画は絶対にあった方が良いです。 スクリーンショットをパシャパシャ撮るだけだと、テストが落ちた時にどこで何がこけてるのか分かりづらいんですよね。 動画を撮ってると、「うわ、画面の読み込みに20秒かかってタイムアウトしてるじゃん」とかも一瞬で分かるので、動画を録画しておくことを強くお勧めします。

Playwright や Cypress であれば公式に対応している(ぽい。使ったことないので動くか不明)し、Puppeteer であれば puppeteer-screen-recorder なるライブラリなどがあり動画を録画することができます。

次にテスト結果の表示場所ですが、これはどこでも良いと思います。

今回のケースでは CircleCI で回していたので、コンソールのログ上にテスト結果が表示されますし、結果をファイルとして artifact に書き出すこともしていました。 テキストも動画も artifact に吐き出して閲覧できるようにしています。

Github Actions でも似たことはできるんですが、GitHub Actions だと artifact が全部 zip で圧縮されてしまうので、結果の中身を見るためにダウンロードして解凍しなくちゃいけないんですよね…… 以下の issue で議論されてるんですけど、どうやら Github の UI 上の制約らしくてまだクローズされてない。悲しい。

github.com

CircleCI は artifact を zip せずにちゃんと生のままずらっと並べてくれるので、ブラウザ上でファイルを閲覧できて便利なんですよね。

あとは、Slack 通知などで実行結果を通知すると親切 & ブラウザテストのプレゼンスが増してよいと思います。 やや政治的な話として、ブラウザテストは役に立ってるのか立ってないのか分かりづらいツールなので、存在感をアピールしておくと良いです。

他には、Datadog ではテスト結果をいい感じに統計を取ってくれるツールがあるので、それを使うのも良いと思います。

www.datadoghq.com

ちなみに僕は使ったことないんですが、そもそも Datadog もブラウザテスト提供してくれてるらしいです。便利な世の中だ……

ブラウザテストのアーキテクチャ・設計をどうするか。

まず、ブラウザテストは本体のアプリケーションとは全く別のアプリとして管理することをお勧めします。 基本的にブラウザテストはユーザーの操作を模倣して、実際の操作通りにテストを行うことを意図しているので、その実装はアプリケーションの実装には(直接的には)非依存であるべきです。 モノレポかどうかなどで変わるとは思いますが、基本的にはアプリケーションごと分けるのがおすすめです。

ブラウザテストのアプリケーションのアーキテクチャを、今回は以下のようなモジュールに分割しました。

  • Browser Emulator
    • 実際にブラウザの操作を行う実体。Puppeteer であったり、 Playwright の chromium であったり。ブラウザの操作を行うくんとブラウザ自体をひとまとめに扱っています。
    • Puppeteer などのインスタンスをそのまま使うのではなく、BrowserEmulator インターフェースを定義してそれによってラップすることで、詳細な実装への非依存を実現しました。
  • Product Knowledge
    • テスト対象のサービスに対する知識。
    • 要するにブラウザを操作する際のセレクタであったり、アクセスすべき URL であったりがここに含まれます。
  • Test Suite
    • 実際のテストケース群。
    • Browser Emulator と input(操作時の入力。たとえば TODO リストのタスク名とか)を受け取って、ブラウザへの操作を行う関数。
    • 具体的には「このボタンをクリックする、テキストフィールドに文字列を入力する、submit ボタンをクリックする」みたいな処理の流れが書かれています。
  • Test Runner
  • config
    • 各種の設定や、fixture(要するに操作時の入力内容)。

細かい実装の話はここでは避けますが、基本的に上のような分割をして、特に Browser Emulator、Test Suite あたりを抽象化するとたとえば Moche <-> Jest を切り替えるとか、Puppeteer から Playwright に切り替えるとかってなったときも、比較的簡単に移行ができます。 実際、最近 Puppeteer だけでなく Playwright を使う必要があるケースが発生し、差し替えが簡単にできました。

あと、もう少し細かい設計の話で言えば、ブラウザテストのセレクタは可能な限り抽象的かつ人間の感覚に沿ったもの(セマンティック)にすべきです。

たとえば、以下のようなセレクタでは何が何だか分かりませんし、ユーザーもこんな階層構造を意識して普段ブラウザを操作しているわけではないはずです。

#editor-main > div > div.buttons.l-editor-footer.js-editor-footer > div.editor-footer-actions > div.editor-footer-save-buttons > span.editor-save-buttons > button

ここでは、もっとシンプルかつセマンティックに、

button/下書きを更新する

のようなセレクタで表現できるべきです。 実際こうした書き方は(文法は違うけど) Playwright であればネイティブの機能として提供されていますし、Puppeteer でも XPath を用いた黒魔術や Aria Selector で実現できます。

複雑な階層構造に依存したセレクタはサービスの変更に弱いですし、実際にユーザーの操作を模倣するという観点でもまずあじです。

シンプルなセレクタでどうしても要素を選択できない場合は、もしかしたら何らかアプリケーションのフロントエンド構造が悪いサインかもしれません。 WAI-ARIA role などの付与を検討しても良いでしょうし、アプリケーションの階層構造に問題があるケースもあります。 こうしたセレクタの設計は、アプリケーションのアクセシビリティ改善にもつながるので一石二鳥です。

まとめ

本記事ではブラウザテストを導入する際に考えるべきことについて記述しました。 ほとんどが経験則や主観によるものですが、意外とこうした細々とした実装ではない設計などの話は転がっていないのではないでしょうか。

もし、ツッコミや「最高のブラウザテストがあって~」といった話があれば是非教えてください。

ありがとうございました。

*1:ここで、ブラウザテストと E2E テストを一緒くたに扱っていますが、あまり細かい字義にこだわらず、Web サービスに対する E2E テストぐらいに思ってください。

*2:ここは、prodution-test とか dev とか色々呼び方あると思いますが、要するに production と似たテスト用環境