第9回 2020年版 ReactにStoryshotsを導入する


1. 概要

Storybookを導入した上で、Storyshotsでレグレッションテストを行うための方法についてです。

2. Storyshotsとは

StoryshotsはStorybookのアドオンで、Storybookに登録されているコンポーネントのSnapshotテストをすることができます。

代表的なテストツールのJest では Snapshot 機能を提供しており、UI コンポーネント毎に Snapshot を記録しておくことで、コードを変更した際の UI 変更を検知できるようになります。

Storyshotsを使うと、Storybookを元にSnapshotファイルを用意できるので、UI 部分に影響が出たのかをすぐに判別できるようになります。
良いので手軽にスナップショットテストを導入することができます。

詳細は以下にあります。

3. インストール方法

事前準備

jestは予めインストールしておく必要がある。
create-react-appで作成した場合、react-test-renderer だけ追加でインストールする必要がある。
その際、react-test-renderer はreactのバージョンと同じものをインストールする必要がある。

$ yarn add -D [email protected]

状況によっては、以下もインストールする必要があるので、各環境で確認ください。

$ yarn add -D babel-jest @babel/preset-react
$ yarn add -D babel-plugin-require-context-hook
$ yarn add -D @babel/plugin-proposal-object-rest-spread
$ yarn add -D @babel/preset-typescript

Storyshotsのインストール

以下コマンドでStoryshotsのインストールが可能である。

$ yarn add -D @storybook/addon-storyshots
$ yarn add -D @types/storybook__addon-storyshots

4. コンフィグの作成

package.json

package.json"test": "NODE_ENV=test jest""storyshots": "NODE_ENV=test jest --config ./jest.config.storyshots.js"を追加する。
jestとstoryshotsはそれぞれ別に実行できるようにする。

package.json
  "scripts": {
    "precommit": "lint-staged",
    "storybook": "start-storybook -p 9009 -s public",
    "build-storybook": "build-storybook -s public",
    "test": "NODE_ENV=test jest",
    "storyshots": "NODE_ENV=test jest --config ./jest.config.storyshots.js"

  },

jest.config.js

jestの設定を追加する。

jest.config.js
module.exports = {
  name: 'client',
  displayName: 'client', // テスト実行中に、ラベルとして console に表示する
  verbose: true,
  moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'], // テスト対象の拡張子を指定する
  transform: {
    '^.+\\.stories\\.tsx$': '@storybook/addon-storyshots/injectFileName',
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/.jest/transform.js',
  },
  testMatch: ['<rootDir>/**/?(*.)(spec|test).(ts|js)?(x)'], // テスト対象のファイル名を正規表現で指定する
  moduleNameMapper: {
    //  import などで指定したファイルが、テストにおいて邪魔になる場合、それを別のモジュールに置き換える
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/.jest/__mocks__/file.js',
    '\\.(styl|css|less|scss)$': '<rootDir>/.jest/__mocks__/style.js',
  },
  // Jest が複数のプロジェクトを独立して扱えるようにする場合は以下のprojectsオプションでconfigを分ける
  //  projects: ['<rootDir>/src/*']
};


transform.jsを作成する。
presetsやpluginはbabel.config.jsに設定しておけばjestにも反映されるが、jestに必要な設定を入れると、Storybookが動作しなかったので、transform.jsに設定を入れる。

環境によっては、
presetsに以下の追加が必要になるかもしれない。
'@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
'@babel/preset-react',
'@babel/preset-typescript',

また、pluginに'@babel/plugin-transform-modules-commonjs'が必要となる場合がある。

jest/transform.js
module.exports = require('babel-jest').createTransformer({
  presets: [['react-app', { flow: false, typescript: true }]],
  plugins: [
    'require-context-hook'
  ],
});

Storyshots用のコンフィグはjest本体とは別に用意する。

jest.config.storyshots.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const baseConfig = require('./jest.config');

module.exports = {
  ...baseConfig,
  testMatch: ['<rootDir>/**/test.storyshots.(js|jsx|ts|tsx)'],
};

jestのモック

モックを作成する

jest/__mocks__/file.js
module.exports = 'test-file-stub';
jest/__mocks__/style.js
module.exports = {};

Storyshotsのプログラム

以下でStoryshotsが動作するようになる。

test/test.storyshots.js
import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots';

initStoryshots({
  // テスト対象を限定する(コンポーネント単位)
  // storyKindRegex: /^LabelSuggest$/,
  // テスト対象を現地する(Story name単位)
  // storyKindRegex: /^StyledButton$/,
  // storyNameRegex: /^Primary$/,
  integrityOptions: { cwd: __dirname },
  test: multiSnapshotWithOptions(),
});

5. エラーの対処

TypeError: Cannot read property 'current' of undefined #151

Reactと異なるバージョンのreact-test-rendererをインストールしているとエラーが出るようです。
同じバージョンをインストールすると解決します。

classnameが毎回変わってしまう

classnameが毎回変わってしまい、差分として上がってしまうのは、JssProviderでclassNameを指定すれば良い。

import JssProvider from 'react-jss/lib/JssProvider';
import { create } from 'jss';
import { createGenerateClassName, jssPreset } from '@material-ui/core/styles';

const generateClassName = createGenerateClassName();
const jss = create({
  ...jssPreset(),
  insertionPoint: document.getElementById('jss-insertion-point') || undefined,
});


const decorateMui = (story: RenderFunction): Renderable | null => (
  <JssProvider jss={jss} generateClassName={generateClassName}>
    {story()}
  </JssProvider>
);

6. その他メモ

スナップショットファイルをストーリーごとに分ける

デフォルトでは全てのストーリーのスナップショットが1ファイルに書き出されるが、ストーリーごとにスナップショットが分かれた方差分が見やすいので、分ける。
multiSnapshotWithOptionsというオプションを指定することで分けることが可能である。
*先述のコンフィグでは既に設定を反映済み。

スナップショットファイルの更新

意図的にHtmlの更新を行った場合、以下コマンドでスナップショットファイルの更新が可能である。

$ yarn run storyshots -u

7. 最後に

今回はFirebase Cloud Functionsを利用して、画像アップロードをトリガーにサムネイルを作成する方法について説明しました。
ですが、実はサムネイルの作成だけであれば、Firebaseの拡張機能で同様のことが実現できます。あくまで学習のためにcloud functionsでサムネイルを作成してみました。

何か更新があれば、追記します。

8. 関連記事

Reactに関する記事です。