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

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

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

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

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

詳細は以下にあります。

https://storybook.js.org/docs/testing/structural-testing/

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

```
$ yarn add -D react-test-renderer@16.8.6
```

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

```
$ 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のインストールが可能である。

```console
$ 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(),
});
```

## 

# ５. エラーの対処
## TypeError: Cannot read property 'current' of undefined #151
Reactと異なるバージョンのreact-test-rendererをインストールしているとエラーが出るようです。
同じバージョンをインストールすると解決します。


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

```typescript
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. その他メモ
## スナップショットファイルをストーリーごとに分ける

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

- https://github.com/storybookjs/storybook/tree/master/addons/storyshots/storyshots-core#multisnapshotwithoptionsoptions


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

```
$ yarn run storyshots -u
```

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

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


# 8. 関連記事

Reactに関する記事です。

- [第1回 2020年版 Node.js+Reactのインストール](https://qiita.com/tetsurotayama/items/6dd91dc37941fab462a4)
- [第2回 2020年版 ReactのMaterial UI V4の使い方について](https://qiita.com/tetsurotayama/items/12d6c7b3463246306e87)
- [第3回 2020年版 React+Firebaseでアプリを作成する](https://qiita.com/tetsurotayama/items/179daca2cacb0caad0c5)
- [第4回 2020年版 既存のウェブサイトに React を追加する](https://qiita.com/tetsurotayama/items/a2e4edd5bc46bc592c32)
- [第5回 2020年版 ReactのRechartsで新型コロナウイルス感染症対策サイトのデータを可視化する](https://qiita.com/tetsurotayama/items/f5eda6dc2bbcd1f71b4e)
- [第6回 2020年版 React+Firebaseで画像のアップロード（その１）](https://qiita.com/tetsurotayama/items/30a0609a0cca5ba5ff0a)
- [第7回 2020年版 React+Firebaseで画像のアップロード（その２）](https://qiita.com/tetsurotayama/items/5129f0cfb21f9ec9b9a0)
- [第8回 2020年版 React+Firebaseで画像のアップロード（その３）](https://qiita.com/tetsurotayama/items/6fb3d3b6e01cac77c85f)