こんにちは、MedPeerの開発を担当している森田です。 今回は私が開発に参画しているMedPeerに元々E2Eテストで利用していたCapybaraと、reg-cliを利用してビジュアルリグレッションテスト(以下VRT)を行える環境を整備したので、それについてご紹介させていただきます。
なぜ、VRTを導入するのか?
MedPeerでは元々System Specを活用したE2Eテストを利用してフロントエンドを含めて品質を担保しておりましたが、デザイン崩れの影響を検知するのは難しく、規模の大きい変更を行う際には手動での画面確認を行っておりました。
しかし、手動での画面確認は検証コストも高く、開発上のボトルネックになりがちであったのと、手動での検証は本来であれば検出されてほしい影響を検知できずにリリースされてしまうこともあり、検証精度を担保しつつスピード感を持った開発を実現する上で課題となっておりました。
そんな中で、VRTを既存のCIに組み込み、デザイン崩れを検知できれば、前述の課題の解決に寄与できるのでは?と思ったのが導入の背景です。
VRTの要件と技術選定
上述の通り、検証精度を担保しつつスピード感を持った開発に寄与するためにVRTを導入するにあたって、必要な要件を以下と考えました。
- コストを掛けずにVRTを記述・組み込めるようにすること
- 幅広くVRTを記述する必要があるので、記述のハードルをなるべく下げるために既存の仕組みの延長線上で実現する
- メインブランチへのマージ前にデザイン崩れの発生に気づき修正できること
- デザインへの影響がPRのstatusで判断でき、CIを落とすことでマージ前に気づいて対応できるようにする
この要件に合わせて、MedPeerではCapybaraとreg-cliを使って構築することにしました。
すでにSystem Specを使って行なっているE2Eテストで利用しているCapybaraの機能であるCapybara::Session#save_screenshot
を使って任意のタイミングでスクリーンショットを取得し、ローカルでも実行できるreg-cliを使って取得したスクリーンショットの差分を検知することで、既存の仕組みを活かしコストを掛けずにCIでデザイン崩れをマージ前に検知できるのではないかと考えました。
実際に構築したVRT基盤の概要
構築したVRT基盤の概要が以下の通りです。
まず事前作業としてspec/systems/visual_regression/screenshots/master
に正となる現時点でのスクリーンショットを取得するSystem Specを作成し、メインブランチに配置しておきます。
そして実際にPRが作成された際に、PRのブランチにて追加したSystem Specを実行し、CI上のPRのブランチで取得したスクリーンショットをspec/systems/visual_regression/screenshots/compare
に配置します。
そして、reg-cli
を使って、それらのディレクトリに配置された同一パス・名称のファイルの差分をチェックし、差分があればCIを失敗させるようにしています。
成功時
失敗時
VRT基盤の具体的な話
System Spec内でスクリーンショットを取得する
System Spec内でCapybaraを使ってVRT用のスクリーンショットを取得できるように以下のHelperを用意しました。
module VrtScreenshotHelper VRT_SCREENSHOT_BASE_PATH = 'spec/system/visual_regression/screenshots' def vrt_screenshot(page, path:, full: true) return unless screenshot_enabled? target = update_master? ? 'master' : 'compare' base_path = screenshot_base_path(target: target) if full save_full_size_screenshot(page, base_path.join(path)) else page.save_screenshot(base_path.join(path)) end end private def save_full_size_screenshot(page, path) original_size = Capybara.current_session.driver.browser.manage.window.size resize_window_to_fit_page page.save_screenshot(path) reset_window_size(original_size.width, original_size.height) end def screenshot_base_path(target:) Rails.root.join(VRT_SCREENSHOT_BASE_PATH, target) end def screenshot_enabled? ENV["VRT_SCREENSHOT_ENABLE"] != "false" end def update_master? ENV["VRT_SCREENSHOT_UPDATE_MASTER"] == "true" end # NOTE: フルサイズのスクリーンショットを取得するためにウィンドウサイズをページに合わせる def resize_window_to_fit_page width = Capybara.page.execute_script(<<~JS) return window.outerWidth JS 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 reset_window_size(width, height) end def reset_window_size(width, height) Capybara.current_session.driver.browser.manage.window.resize_to(width, height) end end
Helper内に実装しているvrt_screenshot
を使うことで利用者側で以下の設定を行いVRT用のスクリーンショットを取得できるようにしています。
- スクリーンショットを配置するディレクトリ
- reg-cliで差分チェックを行うディレクトリ
spec/system/visual_regression/screenshots
を自動設定 - 正となる画像を更新する場合には自動的に
master
に配置する
- reg-cliで差分チェックを行うディレクトリ
- フルサイズでのスクリーンショットの取得
- スクリーンショット取得時に画面サイズをフルサイズに変更してからページ全体のスクリーンショットを取得する
このヘルパーを使って利用する側は以下のような形でフルサイズのスクリーンショットを差分チェックするディレクトリ(spec/system/visual_regression/screenshots/compare/service_name/root_page.png
or spec/system/visual_regression/screenshots/master/service_name/root_page.png
)に自動配置できるようにしました 📸
require 'support/vrt_screenshot_helper' RSpec.describe 'Service name', :js do include VrtScreenshotHelper it 'sample vrt' do visit root_path expect(page).to have_css '.sample-selector' # NOTE: ページが一定表示されるのを待つ vrt_screenshot(page, path: "service_name/root_page.png") end end
reg-cliでスクリーンショットの差分をチェックする
reg-cliを使った以下のスクリプトをpackage.json
に設定し事前作業で取得していた正となる画像とCI(またはローカルでも)上で取得した画像を比較して5%以上の差分があった場合にエラーにするようにしています🕵️
{ "scripts": { "test:vrt": "reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05",
実行しているスクリプトの詳細は以下の通りです。指定できるオプションの詳細は公式のREADMEをご確認いただければと思います。
$ yarn run reg-cli \ spec/system/visual_regression/screenshots/compare \ # チェック対象のスクリーンショットの配置先 spec/system/visual_regression/screenshots/master \ # 正とするスクリーンショットの配置先 spec/system/visual_regression/screenshots/diff \ # 差分を表す画像の出力先 -R spec/system/visual_regression/screenshots/diff/report.html \ # 差分レポートの出力先 -J spec/system/visual_regression/screenshots/diff/reg.json \ # 差分レポート(JSON)の出力先 -T 0.05 # 許容する差分の閾値(%)
実際の実行結果は以下のように確認することができ、差分があった際にはexit code 1.
となりCIが失敗します🍎
yarn run v1.22.22 $ reg-cli spec/system/visual_regression/screenshots/compare spec/system/visual_regression/screenshots/master spec/system/visual_regression/screenshots/diff -R spec/system/visual_regression/screenshots/diff/report.html -J spec/system/visual_regression/screenshots/diff/reg.json -T 0.05 ✔ pass spec/system/visual_regression/screenshots/compare/service_name/root_page.png ✘ change spec/system/visual_regression/screenshots/compare/service_name/sub_page.png ✘ 1 file(s) changed. ✔ 1 file(s) passed. Inspect your code changes, re-run with `-U` to update them. error Command failed with exit code 1.
分かりやすいコマンドでVRTを実行できるようにする
前述までの手順にて、CIでSystem Specを実行しスクリーンショットを取得後にreg-cliでの差分チェックのスクリプトを実行すれば、一定VRTとして機能するようになったかと思います😀
しかし、VRT実行のために複数のスクリプトを手動で実行するのは手間に感じたので、以下のようなRakeタスクを用意してbin/rails visual_regression:run
でSystem Specによるスクリーンショットの取得、reg-cli
による画像比較を実行するようにしました。
require 'optparse' namespace :visual_regression do desc 'Run visual regression tests' task run: :environment do options = {} option_parser = OptionParser.new do |parser| parser.banner = 'Usage: rake visual_regression:run [options]' parser.on('-t', '--target TARGET', 'The directory to run the tests (default: spec/system/visual_regression)') do |v| options[:target] = v end parser.on('-u', '--update', 'Update the master screenshots (default: false)') do |_v| options[:update] = true end parser.on('-h', '--help', 'Show Help') do |v| options[:help] = v puts option_parser.help exit end end # NOTE: OptionParser#order! は optionに存在しない値があるとパースを中断してしまうので、 # rake taskで利用する場合に指定するコマンド名とオプションのセパレーター`--`を削除する # https://docs.ruby-lang.org/ja/latest/class/OptionParser.html#I_PARSE--21 option_parser.parse(ARGV - ["visual_regression:run", "--"]) options[:target] ||= 'spec/system/visual_regression' options[:update] ||= 'false' env = { 'VRT_SCREENSHOT_ENABLE' => 'true', 'VRT_SCREENSHOT_UPDATE_MASTER' => options[:update].to_s, } rspec_success = system(env, 'bin/rspec', options[:target]) # System Specによるスクリーンショットの取得 raise "Get ScreenShot command failed with exit code #{$CHILD_STATUS.exitstatus}" unless rspec_success vrt_success = system('yarn', 'run', 'test:vrt') # reg-cliによるスクリーンショットの差分比較 raise "Check Image diff Command failed with exit code #{$CHILD_STATUS.exitstatus}" unless vrt_success end end
特定のVRTの正となる画像ファイルを更新する際にも以下のコマンドで更新できるようにしました。
$ bin/rails visual_regression:run -- -u -t spec/visual_regression/your_test_spec.rb
CIで差分をチェックする
MedPeerではCircleCIを利用しているので先ほどのRakeタスクを実行し、結果をアーティファクトにアップロードするようなstepを設定することでCI上でVRTを実行するようにしています。
visual_regression: steps: - run: name: run visual regression command: bin/rails visual_regression:run - store_artifacts: path: spec/system/visual_regression/screenshots
CircleCIのアーティファクトはブラウザ上で閲覧できるため、reg-cli
で作成したhtmlレポートを以下のように、そのままブラウザで表示して差分の詳細を確認することができて非常に便利でした 👍
https://github.com/reg-viz/reg-cli/tree/main?tab=readme-ov-file#html-report
OS間での利用フォントによる違いを吸収する
当時MedPeerではfont-family
の指定が以下のようなユーザーのOSフォントを尊重するようなフォント指定になっておりました。
font-family: system-ui, sans-serif;
開発環境はDebian系のOSイメージを利用していますが、CIではUbuntu系のイメージを使用しており、実行環境によって適用されるフォントが変わってしまうことで、ローカルで事前に取得した正となるスクリーンショットとCIで取得したスクリーンショットを比較する現状の方式では、OSによって適用されるフォントが異なるため差分が発生してしまいました。
これが原因でVRTが失敗してしまうことが多かったので、以下のようにVRT実行時にのみtrue
となるカスタムコンフィグを設定して、
Rails.application.configure do # NOTE: VRTのスクリーンショット取得を判別するためのカスタム設定 screenshot_enable = ENV["VRT_SCREENSHOT_ENABLE"] == "true" config.x.visual_regression.screenshot_enabled = Rails.env.test? && screenshot_enable end
以下のようなVRT用のWebフォントを適用するCSSを用意し、
@import "https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap"; body { font-family: "Noto Sans JP", sans-serif !important; }
VRT実行時だけ読み込むことで OS 間のフォント差分を無視できるようにしました。
<% if Rails.configuration.x.visual_regression.screenshot_enabled %> <%= stylesheet_pack_tag 'visual_regression/override' %> <% end %>
おわりに
まだ拡充途中のため具体的な効果までは検証できていませんが、MedPeerにVRT基盤を構築したことによって、手動テストの削減やCSSリファクタリングを安全に行うための環境を整備できるようになりました🎉
MedPeerは医療を扱うサービスのため、こういった仕組みを利用して安定的なサービス提供を実現しつつ、スピード感も維持していきたいです💪
最後まで読んでいただきありがとうございました✨
参考にさせて頂いた資料
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら medpeer.co.jp
■エンジニア紹介ページはこちら engineer.medpeer.co.jp
■メドピア公式YouTube www.youtube.com
■メドピア公式note
style.medpeer.co.jp