storybookのVRTでjest-image-snapshotを試す
始めに
前の記事でChromaticを試してみましたが、pendingステータスが気になったのと、フローが若干冗長になりそうでした。
別な手段としてstorybookからはjest-image-snapshot
が紹介されていたので、この記事ではこれを試した内容をまとめました。
For a self-managed alternative to Chromatic, we offer StoryShots. It allows you to run visual tests on stories by integrating with jest-image-snapshot.
導入方法
storyshotsの導入
まずは以下のようなstoryshotsの設定が動くような環境を用意してください。
import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots';
initStoryshots({
test: multiSnapshotWithOptions(),
});
今回はReactで環境用意しましたが、意外と導入に苦労したので以下の記事も参考にすると良いと思います(現最新パージョンだとまた微妙に違ったエラーにも出くわしたので、時間あったら僕の方でもまとめるかもしれないです)
jest-image-snapshotの導入
ここまで設定できるとあとは簡単で、initStoryshots
のtest
オプションをimageSnapshot
に差し替えるだけでOKです。
+import path from 'path';
-import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots';
+import initStoryshots from '@storybook/addon-storyshots';
+import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer';
initStoryshots({
- test: multiSnapshotWithOptions(),
+ test: imageSnapshot({
+ // ビルド後の出力ファイルを指定するか、storybookにアクセスできるURLを指定する
+ storybookUrl: `file:${path.resolve(__dirname, '../storybook-static')}`,
+ }),
})
storyshotsはこのテストコード1つだけで全storyに対して実行されるので、jest自体も分けて実行すると良いと思います。
import type { Config } from '@jest/types';
import baseConfig from './jest.config';
const config: Config.InitialOptions = {
...baseConfig,
testMatch: ['**/snapshot-image.storyshots.ts'],
};
export default config;
configを指定して実行する場合は以下のようにタスクを書くとできます。
{
"scripts": {
"snapshot-image": "jest --config ./jest.config.snapshot-image.ts"
}
}
あとはpuppeteer
など必要なmodulesをinstallして実行すれば画像のスナップショットが撮れます。
Docker経由で実行させる
これであとはCIにも組み込んで終わりかと思いましたが、1つ問題があります。ローカルの環境とCIの環境でChromiumを使った表示が微妙にずれてしまいます。
例えば以下のような感じで、outlineの色が違っていたり、フォントにずれがあったりします。
このずれを無くすには全く同じ環境でスナップショットを撮る必要があり、つまりDocker経由でスナップショットを撮らなければいけません。
そこで画像のスナップショットが撮れるDocker環境を以下のようなコードで用意しました。とりあえず用意した感じなのでもっと良い書き方があるかもしれませんが、そこはご容赦を。
FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install -y curl
RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
RUN apt-get install -y nodejs chromium-browser fonts-ipafont
RUN npm install -g yarn
WORKDIR /root
version: '3.0'
services:
node:
build: .
tty: true
volumes:
- .:/root
普段使いではあんまりdockerを意識せずに実行させたい(npm scriptsで管理したい)ので、以下のようにshellでタスクをラップしてから呼び出すようにします。
# 外部からの引数も引き継がせるため$@をつける
yarn jest --config ./jest.config.snapshot-image.ts $@
{
"scripts": {
- "snapshot-image": "jest --config ./jest.config.snapshot-image.ts",
+ "snapshot-image": "docker-compose run node /bin/bash ./docker-scripts/snapshot-image.sh"
}
}
shell scriptで$@
としているのはupdateするときの-u
オプションを付与するときに渡せるようにするためです。
$ yarn snapshot-image -u
CIの設定をする
あとはCIの方でも設定します。Dockerfileのもので動かす方法が分かりませんでしたが、docker-composeがデフォルトで使えたのでとりあえず最初から全部docker-composeを使って動かすようにしています。
name: Snapshot
on: push
jobs:
steps:
- uses: actions/checkout@v2
- name: Docker build
run: docker-compose build
- name: Install
run: docker-compose run node yarn install
- name: Storybook Build
run: docker-compose run node yarn storybook:build
- name: Snapshot Image
run: docker-compose run node /bin/bash ./docker-scripts/snapshot-image.sh
その他
CIでdiff画像を保存する
CIでエラーが起きた時はこのままだと以下のようなエラーしか出ず具体的にどういう差分が出ているのか分かりません。
● Storyshots › Example/Counter › Base
Expected image to match or be a close match to snapshot but was 0.16958333333333334% different from snapshot (814 differing pixels).
See diff for details: /root/stories/__image_snapshots__/__diff_output__/snapshot-image-storyshots-ts-storyshots-example-counter-base-1-diff.png
at node_modules/@storybook/addon-storyshots-puppeteer/dist/ts3.9/imageSnapshot.js:26:31
at fulfilled (node_modules/@storybook/addon-storyshots-puppeteer/dist/ts3.9/imageSnapshot.js:5:58)
● Storyshots › Example/Counter › Local
Expected image to match or be a close match to snapshot but was 0.169375% different from snapshot (813 differing pixels).
See diff for details: /root/stories/__image_snapshots__/__diff_output__/snapshot-image-storyshots-ts-storyshots-example-counter-local-1-diff.png
at node_modules/@storybook/addon-storyshots-puppeteer/dist/ts3.9/imageSnapshot.js:26:31
at fulfilled (node_modules/@storybook/addon-storyshots-puppeteer/dist/ts3.9/imageSnapshot.js:5:58)
› 2 snapshots failed.
もちろんローカルで実行したら__diff_output__
ディレクトリに差分画像が出力されるのでそれをみてもらうでも良いですが、GitHubのArtifactを利用することで差分を保存して確認することができます。
jobs:
steps:
- uses: actions/checkout@v2
- name: Docker build
run: docker-compose build
- name: Install
run: docker-compose run node yarn install
- name: Storybook Build
run: docker-compose run node yarn storybook:build
- name: Snapshot Image
run: docker-compose run node /bin/bash ./docker-scripts/snapshot-image.sh
+ - name: Output diff
+ uses: actions/upload-artifact@v2
+ if: failure()
+ with:
+ name: diff-output
+ path: ./stories/__image_snapshots__/__diff_output__
この設定をするとGitHub ActionsのSummaryのところをクリックして、下のArtifacts
セクションからダウンロードすることができます。
ローカルのstorybookにアクセスしてスナップショットを撮る方法
storybookUrl
に稼働中のstorybookのURLを指定すれば良いのですが、localhost
指定はDockerからではアクセスできません。ただ、storybook起動時に出てくるOn your network
を指定するとアクセスすることができます。
╭──────────────────────────────────────────────────╮
│ │
│ Storybook 6.5.9 for React started │
│ 6.15 s for manager and 7.81 s for preview │
│ │
│ Local: http://localhost:6006/ │
│ On your network: http://192.168.0.3:6006/ │
│ │
╰──────────────────────────────────────────────────╯
initStoryshots({
test: imageSnapshot({
// ビルド後の出力ファイルを指定するか、storybookにアクセスできるURLを指定する
- storybookUrl: `file:${path.resolve(__dirname, '../storybook-static')}`,
+ storybookUrl: 'http://192.168.0.3:6006/',
}),
})
ただこれだと毎回コードを書き換える必要があるので以下のようにimport用ファイルを用意して、そこを参照すると良いです。
module.exports = 'http://192.168.0.3:6006/';
initStoryshots({
test: imageSnapshot({
// ビルド後の出力ファイルを指定するか、storybookにアクセスできるURLを指定する
- storybookUrl: 'http://192.168.0.3:6006/',
+ storybookUrl: require('../storybook-url'),
}),
})
使ってみた感想
良いところ
運用フローはスナップショットテストと同じ
既にスナップショットテストをやっている場合は、書き方・やり方はほぼ同じでできるので、扱いやすいと思いました。スナップショットと同じようにdiffのエラーが出て、更新したければ-u
オプションをつけて実行して更新します。diffもスナップショットと同じようにGitHub上で差分を確認できます。
気になるところ
Docker実行のコストが毎回かかる
同じ環境にするため、CIもDockerを毎回呼び出す必要があり、その時間は少し勿体無いなと思いました。少なくとも既にnodeが入っているDocker Imageを使ったらもう少しインストールステップを減らせるのですが、chromium-browser
が上手くインストールできませんでした。この辺はもう少し調整の余地がありそうです。
ローカルでのstorybook対象はビルド済みファイルにすべきか起動中のものにするか
基本的にスナップショットの更新はビルド済みのものにするとビルド忘れで古いファイルでスナップショットを撮ってしまう可能性があるので起動中のstorybookの方が良いが、Docker上だとホスト側のlocalhostにアクセスできません。storybookの起動自体もDockerにする案もありそうですが、パフォーマンスは明らかにローカルの方が早く、個人的にはあんまりDockerに頼りたくはないのでIP指定になりますが、毎回設定ファイルにIPを指定するのも面倒です。storybook起動時に設定ファイルを出力する方法もありそうですが、どんどんゴリゴリの設定を書くことになってしまい、この辺の落とし所が見つかってないです。
まとめ
以上がjest-image-snapshotを使ってみた内容です。個人的にはフローが普通のsnapshotテストと同じでチームに受け入れやすいかなと思いますが、Docker経由で実行したり、storybookが起動中またはビルド済みでないと画像のスナップショットはできないという制約はあったりと多少気にしなければいけないポイントがあるなと感じました。とはいえ新しいサービスを導入するわけでもなく、スナップショットテストを既に取り入れている場合はそこまで設定は大変ではないので導入はしやすいのかなと感じました。
こちらの検証もChromatic導入と同じレポジトリで試しており一緒くたになっておりますが、興味がある方はこちらもご参照してみてください。
Discussion