メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Capybaraとreg-cliを使ってお手軽にビジュアルリグレッションテストを行える環境を整備しました📸

こんにちは、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を使って構築することにしました。

github.com

github.com

すでにSystem Specを使って行なっているE2Eテストで利用しているCapybaraの機能であるCapybara::Session#save_screenshotを使って任意のタイミングでスクリーンショットを取得し、ローカルでも実行できるreg-cliを使って取得したスクリーンショットの差分を検知することで、既存の仕組みを活かしコストを掛けずにCIでデザイン崩れをマージ前に検知できるのではないかと考えました。

実際に構築したVRT基盤の概要

構築した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を失敗させるようにしています。

成功時

CI status(成功時)

失敗時

CI status(失敗時)

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に配置する
  • フルサイズでのスクリーンショットの取得
    • スクリーンショット取得時に画面サイズをフルサイズに変更してからページ全体のスクリーンショットを取得する

このヘルパーを使って利用する側は以下のような形でフルサイズのスクリーンショットを差分チェックするディレクトリ(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レポートを以下のように、そのままブラウザで表示して差分の詳細を確認することができて非常に便利でした 👍

reg-cliによる画像差分のhtmlレポートのサンプル(一覧画面)
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は医療を扱うサービスのため、こういった仕組みを利用して安定的なサービス提供を実現しつつ、スピード感も維持していきたいです💪

最後まで読んでいただきありがとうございました✨

参考にさせて頂いた資料

tech.speee.jp

engineering.linecorp.com


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら medpeer.co.jp

■エンジニア紹介ページはこちら engineer.medpeer.co.jp

■メドピア公式YouTube  www.youtube.com

■メドピア公式note
style.medpeer.co.jp