NIKKEI TECHNOLOGY AND CAREER

ブラウザテストの難しさ「テスト安定化」への道のり

この記事はNikkei Advent Calendar 2021の18日目の記事です。
Cypressによるブラウザテストと、テスト安定化のための取り組みについて日経バリューサーチを例に紹介したいと思います。

実施しているテストの種類

ブラウザテストと言ってもテスト対象の関心はいくつかあります。
日経バリューサーチでは大きく分けると以下の3種類の関心を元にテストを実施しています。

  • ユーザーの利用シナリオに沿ったシナリオを用意して、そのシナリオに沿ってブラウザを動作させるシナリオテスト
  • 発生した障害が再現していないかを確認するための回帰テスト
  • 特殊なログイン方法によって特殊な内部状態になる場合があり、そのような動作時に正常なログインができるかの動作テスト

メインとなるのはシナリオテストで、今は600を超えるテストシナリオが存在しています。
また、特殊ログインに関する動作テストはパラメータの組み合わせによって100程度のテストケースが用意されています。

実行環境

上記全てのブラウザテストはCIとして利用しているCircleCIのスケジュール実行の機能を利用して、平日朝に実施するように設定されていました。
しかしテストの数の増加に比例して実行時間も増加していきます。
またCircleCIは契約の関係上、実行環境のスペックを気軽に上げることができず徐々にテストが不安定になっていきました。
テストが不安定になるとということはつまり稀にテストが失敗するということですが、テストが失敗すると期待する要素が出現するのをタイムアウト上限の時間まで待ったり、テストケース自体のリトライが実施されたりすることで更にテスト時間が増加していきました。

そのためサービスのCI環境とブラウザテストの実行環境を分割し、現在のブラウザテストはAWS CodeBuild上で実施するように変更されています。
CodeBuildであれば契約プランにとらわれずコンピューティングスペックを自由に設定することができます。
またCodePipelineによってCodeBuildの複数のテストを組織化することで、スケジューリングや通知の設定などを一元化することもできました。
気をつけるべきこととしてCI環境でCypressを動かす際の注意点としてよく挙げられていることの繰り返しになりますが、CodeBuildの環境でも同じく日本語フォントを設定する必要があります。
設定方法についてはウェブ上の記事で解説されているものがあるので、ここでは割愛させていただきます。

Jest+PuppeteerからCypressへ

初期のブラウザテストではPuppeteerとJestを組み合わせてテスト実装がされていました。
しかしPuppeteerはブラウザの自動化を目的としているため、テストを目的としたCypressと比べると、テスト実装周りの補助ツールの充実度やテスト自体の書き心地でだいぶ見劣りしていました。

例えばあるボタンをクリックした際に実行されるAPIリクエストの結果を表示する、というUIのテストを書く場合に Puppeteerではボタンをクリックした後に、まず要素が出現しているかを処理として書き下す必要があります。

await page.click("#button");
await page.waitForSelector("#result");
expect(await page.select("#result")).toBe("...");

CypressではAPIリクエストの結果が表示されていることをそのまま処理として書くと、Cypressの内部でアサーション対象としている要素の出現を自動で待ってくれます。

cy.click("#button").get("#result").should("...");

またCypressの開発ツールではテストを動作させつつ途中で動作を一時停止して表示を確認したり、一時停止した状態から開発者が操作して要素を特定するためのセレクターを生成してくれたりする機能があり、これがテストを書く際の障壁をかなり低くしてくれます。
生成されるセレクタはHTMLのid属性や、テスト対象の要素を特定するために利用されるdata-testidなどの属性も加味してくれるので、ほぼそのままテストに生かすことができます。

Cypress開発ツール

餅は餅屋ということで、他サービスでの導入実績がありノウハウも溜まりつつあるCypressに移行を決めました。 幸い、移行を決意した時点ではシナリオテストは未実施、特殊ログインのテストは入力データと期待値がデータセットとしてうまく切り出されていたため、テストケースを動的に生成することで対応でき、移行はスムーズに完了することができました。

長大なテストファイルによる不安定さ

先にも述べましたが、現在のシナリオテストは600を超えるテストシナリオが存在しています。
これほどのテストシナリオが存在していると、テスト分類ごとにテストファイルを分割して整理していても1ファイルのテストが長くなりがちです。
1つのテストファイルの実行にかかる時間が長くなるとCypressのメモリ使用量が増加し、Cypressがクラッシュしてしまうことがありました。
これに対しては単にテストファイルを細かく分割することで対応ができます。
1ファイルのテスト実行数に注意を払いつつ、実際に動かしてクラッシュが起きてしまうような部分に関してはファイル名に通し番号をつけて機械的に分割することで対応しました。

実行時間とアサーションのバランス

一般的に良いテストケースとはそのテストケースでの条件と期待すべき挙動がわかりやすく記述されている状態だと思います。
ユニットテストなどでは正常値を期待する入力条件をテストケースのセットアップ処理で準備し、テストケースごとの入力条件の差分をテストケース内で記述するなどの工夫によって、それらがテスト実装として明確になるように工夫されていたりすると思います。

let testData;
beforeEach(() => {
  testData = {
    input1: "ABC",
    input2: "012"
  };
});

it("正常系の場合は...", () => {
  expect(myFunc(testData)).toBe("...");
});
it("入力1が存在しない場合は...", () => {
  testData.input1 = null;
  expect(myFunc(testData)).toBe("...");
});

上記のサンプルコードであればテストケースごとにtestDataが生成され直すので、テストケースやテスト対象の関数が入力値を破壊的に変更するとしても他のテストケースに影響を与えることはありません。 このような手法はテストの一般論なのでブラウザテストにも当てはまりますが、ブラウザテストはテストケースごとにブラウザやページのセットアップ・サービスのログイン処理などを行う必要があるため、通常のユニットテストなどと比べるとセットアップにかかる時間がとても長いです。
今のシナリオテストは600のテストシナリオが存在しているので、本来であればテストケースも600存在しているべきですが、まともに600のテストを実行しようとすると1テストケース15秒掛かるとしても2時間半も掛かってしまいます。
なので実際には同じようなセットアップを要求するシナリオについては1つのテストケースに纏めていて、複数のアサーションを行うようになっています。
これにより実施テストケース数は600のテストシナリオと比べてずっと少なく、180程度になっています。

テスト結果が確認できる人が一部だけだった

Cypressのダッシュボードではテスト結果はもちろん、テストの実行時間や不安定なテストなど様々なメトリクスを見ることができます。

Cypressダッシュボード

これは日々のテストを実行する上で重要な指標となります。
しかしCypressの契約の関係上このダッシュボードを見れるのはごく一部のユーザーに限られていました。
そのため、テスト結果を元に次の改善アクションを計画することができる人がCypressのダッシュボードを見れる一部の人に限定されてしまっていました。
またCypressの契約はプロジェクト単位ではなく日経組織として契約されていたので、同じ問題が他のプロジェクトでも発生することは容易に考えられました。

そこで、社内の人であれば誰でも結果を見られるようCypressのレポートをHTML出力してS3で社内限定で見れるよう配信するようにしました。

HTMLレポート

Cypressの結果をHTML出力するにはmochawesomeレポーターを使った後に複数のレポートをまとめる処理を行うなど、複数のツールを組み合わせる必要があります
プロジェクト毎に毎回この設定を行うのは煩雑なので、Cypressのレポータープラグインを開発し社内のパッケージレジストリで配布することで他のプロジェクトでも容易にHTMLレポートを出力できるようにしました。
またCypress公式に乗っている方法ではスクリーンショットやビデオ録画などを付与することができないのですが、これにも対応してより的確にテスト失敗の原因調査ができるようにしました。 まだCypressのダッシュボードに比べると実行を跨いだ週次の集計結果などを出すことができていませんが、プロジェクトに関わる人がテスト結果を見る習慣をつけることの第一歩として取り組みました。

それでもタイムアウトしてしまうシナリオ

どうしても安定しないテストは個別に調査を行います。
失敗時のスクリーンショットやビデオ録画を再生して、そもそも要素が出ていないのか・セレクタが正しく動作しているか・APIリクエストはタイムアウトしていないか、などの原因を個別に調査します。
それぞれの調査結果に対して画面の変更にセレクタが追従できていない場合はテストの書き方を見直すなど、個別の対応方針を決めてゆきます。
操作としては正しく実行できているがAPIタイムアウトによって要素が稀に出ないことがある場合など、対応方針としてはひたすら待つことしかできないようなものも有ります。
このような原因のものに対しては個別のテストケースのタイムアウト秒数を伸ばしてやることで対応が可能です。

話は変わりますが、Puppeteerと似たブラウザ自動化ツールのPlaywrightはCypressと似たテスト機能も提供しています。
そのAPIとしてtest.slowがあります。
これは実行しているテストがタイムアウトしがちな場合にタイムアウト秒数を3倍に増やすというものです。

話を戻しまして、Cypressでタイムアウト秒数を伸ばす場合はタイムアウトの原因が何かによって個別のタイムアウト設定を行う必要があります。
タイムアウトの原因を調べることはスクリーンショットやビデオ録画によって難しくはありませんが、ここまで詳細にタイムアウト秒数を設定したいケースは稀と思われます。
そこで、Playwrightのtest.slowを参考にCypressのdescribeitを拡張し、describe.markAsSlowit.markAsSlowのようにテスト単位で一括でタイムアウト秒数を3倍に伸ばすプラグインを開発しました。
このプラグインも現在社内限定でパッケージレジストリよりインストールができる状況になっています。
どうしてもタイムアウトしてしまうようなテストについてはこのプラグインを使って対応するようにしました。

おわり

日経バリューサーチにおけるブラウザテストの取り組みの歴史、テストを安定化させるための取り組み内容、開発したプラグインなどの紹介をさせていただきました。
プラグインについては一般公開も見据えて開発しているので、ドッグフーディングが終わったらnpmレジストリにも登録したいと考えています。
地道な改善内容ではありますが誰かの参考になれば幸いです。

本稿の内容は複数のプロダクトにまたがってテストを行う、テストチームによるブラウザテスト関連の取り組みの一部を紹介したものです。
他の取り組み内容に興味があるという方や、テストを書くことが好きという方は、ぜひお気軽にご連絡ください。

https://hack.nikkei.com/jobs

宮里遼司
ENGINEER宮里遼司

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
短期インターン
Entry