こんにちは、デジタルトランスフォーメーション(DX)事業本部のエンジニアの中嶋(@nyamadorim)です。もともと Rails のサーバサイド開発をメインに担当していましたが、今期はフロントエンド開発に取り組んでいます。
この記事では、「おうちの語り部*1」というプロダクトにビジュアルリグレッションテストを導入して、CSS の改善サイクルが回り始めた話を紹介します。
- ビジュアルリグレッションテストとは
- ビジュアルリグレッションテスト導入の動機
- テストツールの選定
- reg-suit によるテスト環境の構築
- reg-suit によるビジュアルリグレッションテストのフロー
- reg-suit の利用イメージ
- RSpec と Capybara によるスクリーンショットの撮影
- 導入した結果
- 課題
- おわりに
- We are Hiring!
- 参考資料
ビジュアルリグレッションテストとは
ビジュアルリグレッションテストは、ページが視覚的に期待どおりに表示されているかを確認するテスト手法です。公開中のページのスクリーンショットと、開発中のページのスクリーンショットで、ピクセルレベルの差分をとり、意図しない差分が見つかれば、コードの変更によって何らかの不具合が起きたことがわかります。
ビジュアルリグレッションテストで、例えば以下のことをチェックできます。
- レイアウトやスタイルが保たれているか
- コンテンツが正しく表示されているか、あるいは表示されていないか
- ページがエラー画面にならないか、あるいはエラー画面になるか
このように、画像のピクセルレベルで検出できることなら、何でもテストできます。
ビジュアルリグレッションテスト導入の動機
導入の動機は、サービスローンチ時から残ったままだった CSS の負債を解消することでした。負債がある程度洗い出せたため、そのまま CSS をリライトすることもできましたが、ページの見た目が以前と変わらないようにリライトできる自信はありませんでした。無謀なリライトによって、ページのレイアウトが崩れ、ユーザー体験が損なわれることをなるべく避けたいと思いました。
そこで、リライトはやめて、先にビジュアルリグレッションテストを導入することにしました*2。このテストを CI に組み込めば、CSS を変更したときにどのページを壊したかがすぐに分かるので、CSS のリファクタリングを安全に進めることができます。
テストツールの選定
ビジュアルリグレッションテストのためのテストツールを選定した結果、次の理由から reg-suit を採用しました。
- 社内で導入事例があった
- GitHub へのテストレポートの通知が簡単にできる
- RSpec、FactoryBot などサーバサイドで利用しているテストツールをそのまま利用できる*3
- CI に組み込みやすい
reg-suit
reg-suit*4は、ビジュアルリグレッションテストのための OSS です。画像のストア、差分検出、レポート通知などをプラグインと組み合わせて行うことができます。
最大の特長は、テスト対象のアプリケーションのプラットフォームや実装に全く依存しないことです。reg-suit は、特定のディレクトリに保存したスクリーンショットと、AWS S3 や Google Cloud Strage に保存したスクリーンショット画像とを比較し、テスト結果のレポートを生成します。任意の画像ファイルがあればテストができるため、Web アプリであれ、ネイティブアプリであれ、いかなる環境においてもテストが可能です。
reg-suit は、あらゆるプラットフォームのアプリケーションでテストができる代わりに、スクリーンショットの撮影の仕組みを持ちません。そのため、そうした仕組みを自前で用意する必要があります。この点が他のテストツールと大きく違う部分です。
reg-suit がスクリーンショットの撮影の仕組みを持たないことは、一見不便ですが、代わりにサーバサイドで利用しているテストフレームワークをそのまま利用できるメリットがあります。いつも使っている RSpec で テストケースを書いたり、FactoryBot でテストデータを用意することができます。スクリーンショットは Capybara で撮影できます。
他のテストツール
reg-suit 以外に候補としてあがったツール*5は、以下のとおりです。
これらのツールは、テスト対象の URL を設定ファイルで指定すれば、ページを自動でクロールし、スクリーンショットを撮影できる一方、RSpec や FactoryBot を活用できません。
BackstopJS や Wraith では、指定した JS ファイルを実行することで*7*8、テスト条件(Cookie や画面状態)を整えることができますが、Rails アプリのテストデータを用意するのは面倒です。Backstop JS や Wraith から、Rails のモデルやテスト用データベースを参照する手間が必要だからです。
こうした事情から、私たちは reg-suit を採用しましたが、データが一通り揃っている本番環境やステージング環境を対象にテストする場合や、リリース後のテストで構わない場合は、Backstop JS や Wraith が有用だと思います。開発環境やユースケースに応じて最適なツールを選んでください。
reg-suit によるテスト環境の構築
以下の構成で、reg-suit によるテスト環境を構築しました。
- RSpec
- テストランナーとしてテストデータの準備とスクリーンショットの撮影をトリガーする
- FactoryBot
- ページの表示に必要なテストデータを準備する
- Capybara
- スクリーンショットを撮影し、reg-suit が指定するディレクトリに保存する
- CircleCI
- GitHub へのコミットをトリガーに RSpec と reg-suit を実行する
- reg-suit
- 利用プラグイン
- reg-keygen-git-hash-plugin
- Git のコミットグラフをたどり、どの時点のスクリーンショットを比較対象として選ぶかを決める*9
- reg-publish-s3-plugin
- AWS S3 をスクリーンショットの取得・保存、テスト結果のレポートファイルの保存先に利用する
- reg-notify-github-plugin
- テスト結果を GitHub の Pull Request に通知する
- reg-keygen-git-hash-plugin
- 利用プラグイン
reg-suit によるビジュアルリグレッションテストのフロー
上記の構成でテスト環境を構築すると、このようなフローでビジュアルリグレッションテストを行います。
reg-suit の利用イメージ
reg-suit の利用方法をイメージで紹介します。
差分が見つかれば、Pull Request に通知
コミットを GitHub に Push すると、ビジュアルリグレッションテストが実行されます。実画像と期待画像に差分があれば、reg-suit から Pull Request に通知が来ます。ブランチに変更が加わると、通知コメントが自動で更新されます。
赤丸は差分のあったページやコンポーネントなどのスクリーンショット、青丸は差分のなかったスクリーンショットを表します。通知に赤丸があれば、差分が意図したものかを確認します。
どのページ/コンポーネントが変わったかを見る
通知の「this report」リンクをクリックし、どの画像(ページやコンポーネントのスクリーンショット)に差分があったか確認します。
ピクセル単位で差分を確認する
レポートページの「Changed Items」のサムネイルをクリックし、ピクセル単位で差分をチェックします。この例では、アプリの変更によって、ある要素のマージンが大きくなったため、以降の要素がすべてページの下方にずれたことを表しています*10。
確認した結果、意図した差分だった場合は、Pull Request を Approve します。Approve によって reg-suit の Check Status がパス状態に変わります。
差分がなければ ✨✨
reg-suit からその旨の通知が来ます*11。
RSpec と Capybara によるスクリーンショットの撮影
以下は、RSpec と Capybara でスクリーンショットの撮影を行うコードです。expect がないこと、スクリーンショットを撮影していること以外は、普通の RSpec コードと変わりません。
RSpec.feature 'CorporatesController', type: :system, js: true do describe '#show' do describe 'ビジュアルリグレッションテスト' do it do # ページの表示に必要なデータを用意 # テストデータの生成に Faker などを利用している場合は、ランダム値がページに含まれないよう気をつけてください corporate = create(:corporate) # ページを表示 visit corporate_path(id: corporate.id) # ページの表示完了を適当な秒数だけ待つ(後述) sleep 5 # スクリーンショットを撮影する # 引数の文字列は、どのページに変更があったかがわかる名前にします take_full_page_screenshot('corporates-show') end end end end
以下が、スクリーンショットを撮影するためのヘルパーモジュールで、ページ全体のスクリーンショットを撮影する take_full_page_screenshot
メソッドを定義しています。このモジュールは、spec/rails_helper.rb から require して利用します。
module SystemSpecHelpers # ページ全体のスクリーンショットを撮影する def take_full_page_screenshot(name) # ページのサイズに合わせてウィンドウをリサイズ resize_window_to_fit_page # PNG 形式のスクリーンショットデータを変数に代入 img = page.driver.browser.screenshot_as(:png) # `screenshot/` ディレクトリに撮影した画像を保存 FileUtils.mkdir_p(Rails.root.join('screenshot')) File.open(Rails.root.join('screenshot', "#{name}.png"), 'wb') do |f| f.write img end end # ページの幅と高さを取得し、それに合わせてウィンドウのサイズを変更する def resize_window_to_fit_page # ページの幅を取得 width = Capybara.page.execute_script(<<~JS) return window.outerWidth JS # ページの高さを取得 # 縦方向にページが途中で切れないように、各 API で取得した高さの中で、最も大きいものを採用する(多少余白が出ても気にしない) # 参考: https://testingrepository.com/full-page-screenshot-with-selenium/ height = Capybara.page.execute_script(<<~JS) return Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight); JS # 指定した幅と高さでウィンドウをリサイズ Capybara.current_session.driver.browser.manage.window.resize_to(width, height) end end
導入した結果
ビジュアルリグレッションテストを導入したことで、Rails アプリの CSS のリファクタリングを毎週のペースで安全にできるようになりました。
このテストを導入する以前は、ページの表示がおかしくならないことを祈りながら、CSS を書き換えるしかなく、安全にリファクタリングできる環境はありませんでした。
ビジュアルリグレッションテストを導入した結果、CSS もサーバサイドのコードと同じように、テストを補助輪にして安全に改善が行えるようになりました。自動テストが当たり前になっている令和の時代に、CSS の開発環境がようやく追いついたわけです。
課題
ビジュアルリグレッションテストは、今のところいい感じに機能していますが、課題は残っています。
テスト時間の増加
現在、sleep
によってページのレンダリングの完了を待ってから、スクリーンショットを撮影しているので、これによって、撮影対象のページが増えるごとに sleep
の待機時間が積み重なり、テストの時間が増加してしまいます。
他社の事例では、Selenium の代わりに Puppeteer を使うことで、sleep
よりもベターな方法でこの問題に対応しています。具体的には、Puppeteer のメトリクスによって、ブラウザのレンダリングが落ち着いたタイミング(=レンダリングが完了)を見計らって撮影していますが、残念ながら Capybara は、Selenium ベースのため、Puppeteer のような細かいメトリクスが取れません。
根本的に解決したい問題ではあるものの、現状はテスト対象のページのパターン数が少ないこと、今後もそれほど増えないこと、ページ数が増えたとしても GitHub Actions 等でビジュアルリグレッションテストを他のテストと並列化できることから、後回しにしています。
ビューの低いテスト容易性
これは、ビジュアルリグレッションテストの課題ではなくアプリ設計の課題ですが、ビジュアルリグレッションテストを通して、潜在的な課題に気づくことができました。
ビジュアルリグレッションテストを導入したプロダクトでは、同じ企業がずっと上位に表示されることを防ぐために、企業一覧をシャッフルしている場所があります。常に同じスクリーンショットが得られるよう、テスト時にはシャッフルを無効化する必要があります。しかし、現状の実装では、シャッフルのロジックが Decorator に直書きされており、かつ共通化されていなかったため、表示ロジックをテスト時に差し替えづらい状態でした。
現在は、無理やりですが Array#sample メソッドを RSpec モックで差し替えて対応しています。
allow_any_instance_of(Array).to receive(:sample, &:first)
React や Vue.js のコンポーネントのように、コンポーネントの属性に表示する企業一覧を渡せたら良いのですが、Rails のビューだと、このようにメソッドを mock するか、テスト環境でシャッフルを無効化するように条件分岐する必要があります。
おわりに
おうちの語り部という Rails 製プロダクトに、ビジュアルリグレッションテストを導入しました。reg-suit の選定理由と利用イメージを紹介し、RSpec + Capybara でのテスト方法を説明しました。課題はまだ残っていますが、このプロダクトのチームでは、ビジュアルリグレッションテストを補助輪に CSS のリファクタリングを継続できており、とてもいい感じです。
reg-suit は、アプリのプラットフォームに依存しないコンパクトな作りですが、プラグインによって GitHub 通知といった細かいニーズにも対応できる、バランスの取れたテストツールだと感じました。差分のレポートもわかりやすく、今後社内の他のプロダクトにも利用したいと思います。
We are Hiring!
私たちは一緒に働いてくれる仲間を募集しています!
参考資料
*1:不動産売却経験者による不動産会社のレビューサイトです
*2:おうちの語り部は、ページのパターン数が少ないため、頑張ればリライトできなくはないですが、長期的な開発資産を残すことを重視し、ビジュアルリグレッションテストを導入しました
*3:Rails アプリならではの事情ですが、フロントエンドとサーバサイドを完全に切り離せる実装になっていないため、Rails モデルなどサーバサイドのコードを直接呼び出せる環境で、テストを実行できる必要があります
*4:私は「れぐすーと」と呼んでいます
*5:テストツールの候補は、1px の変化も見逃さない!ビジュアルリグレッションテスト導入で快適フロントエンド開発 - クラシル開発ブログを参考にさせていただきました。
*6:読みは「れいす」
*7:http://bbc-news.github.io/wraith/index.html#before_capturehooks
*8:https://github.com/garris/BackstopJS#running-custom-scripts
*9:このプラグインは、トピックブランチの派生元のコミット時点のスクリーンショットを、比較対象として選ぶアルゴリズムになっています
*10:スクリーンショットに表示されている不動産会社名は、テスト用のダミーテキストです
*11:どうでも良い話ですが、私はこの通知が心地よくてとても好きです✨✨