Gatsby + storycap + reg-suit でビジュアルリグレッションテスト

Re: ゼロから始めるビジュアルリグレッションテスト生活

Soudai Sasada

Soudai Sasada

Gatsby.jsで作られたサイトにstorycapとreg-suitでビジュアルリグレッションテストを導入してみました。

先にやってみた感想を述べてしまうと「普段からstorybookをメンテできているならやるだけの価値はある」というところです。これは端的に言ってstorycapとreg-suitがとても素晴らしく、一度セットアップしてしまえば運用面のコストが低くなるように設計されており、また、セットアップの手間も最小限で済むようになっているためです。

リリース時のUI崩れ対策としてどこまで時間を掛けるかはそのプロダクトが抱えている開発リソースや事業フェーズにも左右されるかと思います。操作さえできているならばスピードを優先しUIの崩れは許容するという判断もあるでしょう。しかしだからといって実際にUI崩れが発生した際にそれを無視できるかは別問題です。もしUI崩れが発生した箇所を担当していて自身の修正によりそれが発生したことが分かったら罪悪感を抱いたり修正したくなるのではないでしょうか。
個人的には安心感を買うというたった一つの目的だけだったとしても導入する価値はあると感じました。もし普段からstorybookをメンテできていてUIに関する自動テストが導入されていないようでしたらぜひ導入を検討してみることをお勧めします。

今回はどのようにしてセットアップできるのかについて簡単に紹介したいと思います。

storybookのインストール

storybookはコンポーネントをカタログ化して閲覧するためのライブラリです。

まずはドキュメントに従いnpxコマンドでstorybookをインストールします。

$ npx -p @storybook/cli sb init

このコマンドによりpackage.jsonにstorybook起動用のコマンドが追記されます。
また、.storybookというディレクトリとその配下にmain.js,preview.jsというファイルが作られ、src/storiesというディレクトリにサンプル用のstoryが用意されます。
これらによりstorybookが起動できるようになり、同時にサンプル用storyをstorybook上で閲覧することができるようになっています。

Gatsby.jsのプロジェクトではドキュメントのままでは動かないので修正が必要となります。webpackに関する設定を.storybook/main.jsに追記しましょう。
なお筆者はscssを使用していたのでここでドキュメントにある設定のほかにsass-loaderに関する設定を追記しています。

.storybook/main.js
module.exports = {
  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ],
  "webpackFinal": async config => {
    // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
    config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

    // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
    config.module.rules[0].use[0].loader = require.resolve('babel-loader')

    // use @babel/preset-react for JSX and env (instead of staged presets)
    config.module.rules[0].use[0].options.presets = [
      require.resolve('@babel/preset-react'),
      require.resolve('@babel/preset-env'),
    ]

    config.module.rules[0].use[0].options.plugins = [
      // use @babel/plugin-proposal-class-properties for class arrow functions
      require.resolve('@babel/plugin-proposal-class-properties'),
      // use babel-plugin-remove-graphql-queries to remove static queries from components when rendering in storybook
      require.resolve('babel-plugin-remove-graphql-queries'),
    ]

    // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
    config.resolve.mainFields = ['browser', 'module', 'main']

    // https://github.com/storybookjs/storybook/issues/5708
    config.resolve.extensions.push('.svg')

    config.module.rules = config.module.rules.map(data => {
      if (/svg\|/.test(String(data.test)))
        data.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani)(\?.*)?$/

      return data
    })

    config.module.rules.push({
      test: /\.svg$/,
      use: [
        { loader: require.resolve('babel-loader') },
        { loader: require.resolve('react-svg-loader') },
      ],
    })

    config.module.rules.push({
      test: /\.scss$/,
      use: [
        { loader: require.resolve('style-loader') },
        {
          loader: require.resolve('css-loader'),
          options: {
            importLoaders: 1,
            modules: true,
            sourceMap: true,
          },
        },
        { loader: require.resolve('sass-loader') },
      ]
    })

    return config
  },
}

なおGatsby.jsの場合webpack用のconfigファイルは必要はありません。.storybook/main.jsでwebpackの設定ができますのでこちらを使いましょう。

storycapのインストール

storycapはstorybookをクローリングし各storyのスクリーンショットを保存するライブラリです。こちらの記事に誕生の経緯まで含めて詳しく記載されています。

使い方は非常に簡単でインストールしたらコマンド一発で動いてくれます。

$ yarn add -D storycap
$ npx storycap http://localhost:6006 # storybookのURLを指定

使い方はドキュメントに詳しく載っています。storybookのキャプチャに特化したライブラリなので対象はローカルサーバでもそうじゃなくてももちろんオッケーです。

使い方は非常に簡単でstorycapではstorybook用のdecoratorとしてwithScreenshotという関数が用意されておりこれを使えばstorybook上で画面に関する様々なパラメータを渡すことが可能になります。
たとえばviewportでは画面サイズや画面の向き(縦/横)を直接指定できるほか(これはPuppeteerに定義されているviewportとなります)、具体的な値の代わりに各種デバイス名を渡せばそのデバイスサイズでのキャプチャが可能で、1つのstoryにつきデフォルトでは1つのキャプチャを取得しますがvariantsを使えば指定した値の組み合わせによる複数のキャプチャを取得できるため「story x 状態 x デバイス」のように網羅的なキャプチャができます。

reg-suitのインストール

reg-suitはビジュアルリグレッションテストをCLIで実現するためのライブラリです。
特筆すべきはビジュアルリグレッションテスト以外の責務を持たないため検証する画像の生成方法を問わない点です。これによりstorycapを使わないでも画像さえあれば差分のチェックが可能です。
たとえばRailsでCapybaraを使ってE2Eテストを実行しつつUIについてはテスト実行中に保存した画像を元にreg-suitによるビジュアルリグレッションテストを実行しチェックする、なんてこともできるということですね。

では早速セットアップしましょう。パッケージ管理にyarnを使っている場合はinitコマンドで--use-yarnオプションを付けるのを忘れずに。
対話形式でインストールするプラグインの選択やreg-suitが利用するディレクトリ、差分をチェックする対象画像が格納されているディレクトリ、差分を判定する閾値を指定していきます(これらの質問を元にregconfig.jsonという設定ファイルを書き出すだけなので後から変更可能です)。

$ yarn add -D reg-suit
$ reg-suit init --use-yarn # プラグインのインストールを含めたセットアップ

筆者は以下のプラグインをインストールしました。

  • reg-keygen-git-hash-plugin
  • reg-publish-s3-plugin
  • reg-notify-github-plugin
  • reg-notify-slack-plugin

reg-keygen-git-hash-pluginはgitのコミットハッシュをキーに過去のコミットグラフから良い感じに比較対象を選別してくれるプラグインです。

reg-publish-s3-pluginはコミット時点の画像をs3に格納するためのプラグインです。CIで差分チェックを行うにはこれかreg-publish-gcs-plugin を使わないと過去の画像と比較できないためほぼ必須と言えるでしょう。ローカルで画像を管理するのも容量の都合で限界がありますしセキュリティ面からもアクセス権限設定が細かく行えるs3やgcsは適切だと思います。

reg-notify-github-pluginはCI上で差分チェックした際にPull RequestがあればそのPull Requestへのコメントとしてチェックの結果を投稿してくれるプラグインです。オプションでPull Requestのコミットのステータスを変更したり新しいコミットがあった場合にコメントを追加で投稿するか過去のプラグインによる投稿を上書きするかといった振る舞いを変更できます。
利用する場合はreg-bizが提供しているGithub Appをリポジトリにインストールしましょう。 実際にはCLI上で対話を進めていけばブラウザでGithub Appのインストールページが開くのでそのまま許可するだけです。

実際の画面。

connecting-reg-suit-app

CLIではGithub AppのクライアントIDの入力を求められるのでブラウザ上でGithub Appインストール後に表示されるクライアントIDをコピーしペーストすれば準備が整います。

reg-suit-app-connected

reg-notify-slack-pluginはSlackのincoming Webhook用URLに通知するためのプラグインです。もしreg-notify-github-pluginを導入していてGithubがすでにSlackと連携しているのであればそちらで確認すれば良いので必要ないでしょう。

ここで紹介していないプラグインや紹介しているプラグインの仕様や設定について詳しく知りたい場合はドキュメントをご覧ください。

CircleCIのセットアップ

それではいよいよ継続的インテグレーションでビジュアルリグレッションテストを自動実行し結果を確認するための環境を整えましょう。

この記事は無料で利用でき筆者が普段から使い慣れていることからCircleCIでのセットアップについて説明したいと思います。

まずはビルド済みDockerイメージを選択します。CircleCIではイメージ名に-browsersというバリアントでブラウザがバンドルされたイメージを提供しています。storycapでstorybookをクローリングするためにはブラウザが必要なためまずブラウザ付きのビルド済みイメージを探しましょう。
これはイメージのスピンアップ後にブラウザをダウンロードするとCircleCIの実行時間が大きく伸びてしまうためです。

もしブラウザがバンドルされたイメージがない場合はブラウザ(Chrome)をインストールしましょう。
CircleCIでは部品化された構成を再利用できるOrbsというパッケージ機能が提供されています。直接DLするスクリプトを書いても良いですが、今回必要なChromeのようにCircleCIが公式パッケージを提供しているケースもあり、これを使えばわざわざ面倒な設定を書く必要はなくなります。特にこだわりがなければOrbsを使ったインストールをお勧めします。browser-toolsというOrbでChromeを含め指定したブラウザをインストールするためのスクリプトが用意されていますのでこちらを使うと良いでしょう。

余談にはなりますがほかにもaws-cliなど便利なパッケージが用意されているのでCircleCIの設定を変更する際には一度どんなパッケージがあるかご覧いただくと良いかもしれません(Orbs一覧)。

また現在CircleCIではCIに特化した次世代型のビルド済みイメージが用意されています。これは端的にいえば「より便利なやつ」といったところです。以下、公式ドキュメントからの引用です。

新しいコンビニエンス イメージは、CI、効率性、確定的動作を念頭に置いてゼロから設計しました。注目ポイントを紹介しましょう。

  • スピンアップ時間の短縮 – Docker 的な言い方をすれば、次世代イメージは概してレイヤーがより少なく、より小さくなっています。これらの新しいイメージを使用すると、ビルド開始時にイメージがすばやくダウンロードされると共に、イメージが既にホストにキャッシュされている可能性が高くなります

  • 信頼性と安定性の向上 – 従来のイメージは、アップストリームからの変更によってほぼ毎日再ビルドされるため、テストが間に合わないこともあります。そのため、互換性の損なわれる変更が頻発してしまい、安定した確定的なビルドに最適な環境とは言えなくなっています。次世代イメージは、セキュリティと致命的なバグについてのみ再ビルドされるため、より安定した確定的なイメージとなります。

https://circleci.com/ja/blog/announcing-our-next-generation-convenience-images-smaller-faster-more-deterministic/

CIの実行時間は生産性に影響しますし信頼性や安定性は高いに越したことはないですね。特に制約がなければcimg/で始まる次世代型イメージを使うのが良さそうです。
なお次世代型イメージはCircleCIがゼロから作っているということもあり全てのバージョンがあるわけではありません(現に筆者が動かしていたnodeバージョンでは執筆時点においてブラウザがバンドルされたバリアントがありませんでした)。もともとCircleCIが提供しているコンビニエンスイメージ(ciecleci/で始まるもの)も大きく見劣りするわけではないので次世代型のブラウザがバンドルされたイメージがなければ無理に次世代型を使わずこれまでのブラウザがバンドルされたコンビニエンスイメージを使う方が良いかもしれません。
どちらの方が速いか気になったため筆者が実際に試したところバンドルされていない次世代型イメージでChromeをダウンロードする場合には従来のブラウザがバンドルされたコンビニエンスイメージより余分に30秒前後の時間が掛かりました。次世代型はイメージそのもののキャッシュヒット率が高いはずなので一概には言えませんが、キャッシュヒットを期待するよりも安定して速い方が個人的には好みです。もし次世代型イメージをこの時間を短縮したいならCircleCIのキャッシュを使いブラウザをキャッシュするなどなんらかの工夫が必要になるでしょう。その手間を掛けてでも次世代型イメージを使いたいかが判断のポイントになりそうですね。

ブラウザ付きイメージの場合

.circleci/config.yml
version: 2.1
executors:
  node-executor:
    working_directory: ~/workspace
    docker:
      - image: circleci/node:13.12-browsers # ブラウザ付きイメージを指定
jobs:
  visual_regression:
    executor:
      name: node-executor
    steps:
      - checkout
      - restore_cache:
          name: Restore node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Install dependencies
          command: yarn install
      - save_cache:
          name: Cache node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
      - run:
          name: Install JP fonts
          command: sudo apt-get update && sudo apt-get install fonts-ipafont-gothic fonts-ipafont-mincho
      - run:
          name: Save screenshots
          command: yarn storycap
      - run:
          name: Run visual regression test
          command: yarn reg-suit --quiet
workflows:
  version: 2.1
  test:
    jobs:
      - visual_regression:
          filters:
            branches:
              ignore:
                - master

ブラウザのないイメージ + ブラウザをダウンロードする場合

.circleci/config.yml
version: 2.1
executors:
  node-executor:
    working_directory: ~/workspace
    docker:
      - image: cimg/node:13.12 # ブラウザのないイメージ
orbs:
  browser-tools: circleci/browser-tools@1.1.0
jobs:
  visual_regression:
    executor:
      name: node-executor
    steps:
      - checkout
      - browser-tools/install-chrome # browser-toolsに定義されているコマンドでChromeをダウンロード
      - restore_cache:
          name: Restore node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
      - run:
          name: Install dependencies
          command: yarn install
      - save_cache:
          name: Cache node_modules
          key: yarn-{{ checksum "yarn.lock" }}-{{ .Environment.CACHE_VERSION_NPM }}
          paths:
            - ~/workspace/node_modules
      - run:
          name: Install JP fonts
          command: sudo apt-get update && sudo apt-get install fonts-ipafont-gothic fonts-ipafont-mincho
      - run:
          name: Save screenshots
          command: yarn storycap
      - run:
          name: Run visual regression test
          command: yarn reg-suit --quiet
workflows:
  version: 2.1
  test:
    jobs:
      - visual_regression:
          filters:
            branches:
              ignore:
                - master

これでひととおりの設定が完了しました。
ご覧いただいたとおりstorycapやreg-suitが素晴らしいためほぼインストールだけでセットアップができました。すでにstorybookを利用しているプロジェクトであれば比較的容易にビジュアルリグレッションテストを導入できるのではないでしょうか。

自動テストの素晴らしい点のひとつは検証されている対象が予め想定した振る舞いができるか検証してくれるところです。そしてビジュアルリグレッションテストでは「UI崩れ」が起きていないかを「視覚的」に検証することができます。reg-suitの機能によりチェックする対象は設定した閾値を超える差分があるものだけに絞り込まれるため運用コストの点でもリーズナブルで、スナップショットテストと違い外部の影響で視覚的にコンポーネントのUI崩れが起きているような事象も検知できます。
一方でビジュアルリグレッションテストは振る舞いまで検証してくれるものではありません。あくまでビジュアル面のみなため、たとえばボタンが押しても動かなくなっているといったデグレまでは検知できないでしょう。また、story化されていないコンポーネントがあったとすればそれもチェックの対象から外れます。これらはE2Eテストなど別のアプローチで検証していくのが良さそうです。

今回初めてビジュアルリグレッションテストを導入してみましたが想像以上に簡単に期待した効果を得られそうでしたので紹介させていただきました。この記事がこれからビジュアルリグレッションテストを導入しようとしている方のお役に立てれば幸いです。

この記事がお役に立てたら一杯のコーヒーを恵んで頂けると励みになります 😊

Buy Me A Coffee
© 2020, Soudai Sasada