明けましておめでとうございます。
エンジニアブログを初めて、ちょうど1年になりました。 去年はなんとか月に1記事が書ける程度でしたが、今年は月に最低5記事は書いていきます。
今年最初の記事としては、弊社で最近取り組んでいる「Visual Regressionテスト」について紹介します。
Visual Regressionテストとは
私自身は馴染みのない言葉でしたが、以下のサイト等を見ると「Webページのスクリーンショットを以前のバージョンと比べて、ピクセルレベルでの差分を検出するテスト手法」と定義されているようです。
HTML,CSSの修正では、予期せぬ部分に影響することがよくあると思います。それを防ぐために手動によるテストだけに頼ると、サイトの規模が大きくなるにつれてコストが増加しますし、軽微な差であれば目視では気づけ無いこともあります。
一方、 Visual Regressionテストは、低コストで確実に差分を検出することが可能です。例えば、以下のような目視では気づきにくい微妙な差も、スクリーンショットを比較すれば明確に分かります。
実装方法
Visual Regressionテストの実装は、PhantomCSSやBackstopJSを利用すると比較的容易です。
GitHub - HuddleEng/PhantomCSS: Visual/CSS regression testing with PhantomJS techblog.lclco.com
上記の記事の通り、弊社も以前はBackstopJSを利用していましたが、後述の確認レポート等を作成するために、使い慣れたRubyを中心とした以下の技術を用いています。
- Ruby + Capybara + Poltergeist
- Webページアクセス、スクリーンショット取得
- Rmagick
- 差分画像の生成、画像の差異情報の取得
- Amazon S3
- スクリーンショットの保存
- PostgreSQL
- テスト結果の保存
テストの流れ
テストの流れは下記のようになっており、担当者はHubotに命令と、実行結果の確認だけすれば良いだけです。
- 実装担当者が、Hubot に命令しテストJOBの実行
- 各ページのスクリーンショットを取得し差分比較
- 結果をS3,DBに保存
- 結果レポートのURLをチャットへ通知
- 実装担当者が、結果を確認する
各所について、補足していきます。
テスト実行のタイミング
当初、GitへのPUSHをトリガーに自動でテストを実行していましたが、WIP段階でのPUSHや、関係のないサーバサイドの修正でもテストが実行され、チャットへの通知が煩雑になりました。
現在は、以下のタイミングで実行しています。
- 開発中は、任意タイミングで実行 ( Hubotに実行命令を出す)
- Staging環境へのデプロイ時には自動実行
テスト対象ページ
予期しないページへの影響を検出するのが、目的の一つであるため、全てのページをテスト対象にするべきと考えてます。(検索ページなどパターンが多すぎるページは、代表ページのみに絞る)
弊社では、GoogleスプレッドシートでサイトのURL一覧を管理しており、そのシートをからにテスト対象URLを取得しています。新しいページを作成する場合は、このシートをメンテナンスする運用にしているため、シートに追加すれば自動的にテスト対象に加わるようになっています。
比較対象
Visual Regressionテストでは、何を正として比較するのが重要であり、比較対象は以下の条件が必要と考えています。
- 比較元は、正しい仕様が実装されている
- 比較元と比較先は、同一のデータソースを元にページが生成される。(動的ページの場合は、データソースを同じにしなければ、データによる差異が大量に発生してしまいます)
弊社では、実行されるタイミングによって、比較対象を変更しています。
- 開発中の実行時は、現在のブランチの環境*1とStaging環境を比較
- Stagingのデプロイ時の実行時は、前回のStaging環境での実行結果とデプロイ後のStaging環境の実行結果を比較
テスト対象となる全ての環境は、同一のデータベースを参照しており、Staging環境はProduction環境と同じmasterブランチのコードで動いています。これは、masterブランチのコードは正しい仕様であるという前提に立っています。一度masterに不具合が混入した場合、気づかなければ今後も間違い続けるという問題を抱えており、それをガードする仕組みは現在も検討中です。
スクリーンショットの差分比較
スクリーンショットの取得は、poltergeistのsave_screenshotを利用しています。
GitHub - teampoltergeist/poltergeist: A PhantomJS driver for Capybara
スクリーンショットの差分比較は、Rmagickのcompositeを利用しています。 比較元・比較先の画像から、差分画像が作成されます。
before_image = Magick::ImageList.new("/tmp/before.jpg") current_image = Magick::ImageList.new("/tmp/current.jpg") diff_image = before_image.composite(current_image, 0, 0, Magick::DifferenceCompositeOp)
差分画像は、黒のピクセルが一致、黒以外が不一致箇所として生成されます。
また、differenceメソッドで差異の数値を求めることも可能です。戻り値の結果が0でなければ差異ありと判断しています。
before_image = Magick::ImageList.new("/tmp/before.jpg") current_image = Magick::ImageList.new("/tmp/current.jpg") diff_rate = before_image.difference(current_image) pp diff_rate > [4129.06884765625, 0.03533638422297351, 1.0]
テスト結果の確認
Visual Regressionテストでは、発生した差分が適切かどうかの最終チェックは目視になってしまうため、できるだけ簡単に結果が確認できる仕組みが重要だと考えてます。
弊社では、テスト結果をS3,DBに保存し、Web上で一覧表示できるようにしています。(テスト完了後に、確認用URLがチャット通知されます)
(左から差分画像、比較元、比較先)
また、ページごとの差分値も表示し、デフォルトは差分なしの画像は表示されません。担当者は、チャットに通知された確認用のURLを開いて、差分がある画像をさっと結果を確認するだけでチェックが完了します。
最後に、実装する上でのTIPSをいくつかご紹介いたします。
Ajaxの実行完了を待つ
Ajaxを利用して非同期でページを組み立てている場合は、全てのAjaxの完了を待ってスクリーンショットを取得する必要があります。 jQueryを利用している場合は、以下のようにjQueryの実行完了までwaitさせるとAjax実行後の画面が取得できます。
loop until finished_all_ajax_requests?(session) def self.finished_all_ajax_requests?(session) return true if session.evaluate_script('jQuery.active').nil? session.evaluate_script('jQuery.active').zero? end
一部のエリアだけスクリーンショットを取得する
save_screenshotメソッドの引数に、CSS selectorを記載すると該当箇所のスクリーンショットが取得できます。
save_screenshot('/path/to/file.png', :selector => '#id').
動的な広告を除外する
ページ上に動的な広告を表示していると、アクセスするたびに表示される広告が異なり、毎回差分として検出されてしまいます。
poltergeist 1.10から追加されたホワイトリスト・ブラックリスト機能で、広告が配信されるシステムのドメインを除去することで、この問題の対策ができます。
弊社では、ホワイトリストに自ドメインと外部JS・画像配信で利用しているいくつかのCDNを追加しています。
session.driver.browser.url_whitelist = ["テスト対象ドメイン","xxx"]
また、余計なファイルのロードも抑制できることで、テスト時間の短縮につながります。
まとめ
Visual Regressionテストを運用してから、テスト工数の削減ができ、何よりも安心してリリースができるようになりました。特にリファクタリングをする際に有用で、思い切ったリファクタリングに取り組めるようになりました。Visual Regressionテストは、一度仕組みを作ってしまえば、あまり運用コストもかからず、一定の品質担保に役立ちます。本記事が、皆様の参考に少しでもなれば幸いです。
*1:ブランチをPUSHすると、テストサーバにそのブランチ専用の環境を生成するようにしています。これについては別途記事を書きたいと思います。