NuxtプロジェクトにStorybookとStoryshotsを導入してcomponentをテストする

## はじめに
試験的にNuxtプロジェクトにStorybookとStoryshotsを導入してみました。いくつかはまったところがあるので、今回、導入したときの手順を残しておきます。主なパッケージのバージョンは以下の通りであり、バージョンによって挙動が変わるのでお気を付けください！

    "nuxt": "^2.13.0"
    "@storybook/addon-storyshots": "^5.3.19",
    "@storybook/addon-storyshots-puppeteer": "^5.3.19",
    "@storybook/vue": "^5.3.19",
    "babel-core": "7.0.0-bridge.0",
    "babel-plugin-require-context-hook": "^1.0.0",
    "jest": "^25.5.4",
    "puppeteer": "^5.0.0",

また、今回作成したサンプルプロジェクトは[github](https://github.com/EsashikaAtsushi/storybook-nuxt-sample)にあげているので、詳細を確認したいときはこちらにアクセスくださいm(__)m

## Nuxtプロジェクトの作成
はじめにに、Nuxtプロジェクトを作成します。今回はUIにBulmaを使ってみます。
実際にプロジェクトを作っていくときは、ディレクトリ構造をデフォルトから変えていくことが多いと思うのですが、今回はあくまでもStorybookなどにフォーカスするため、そのままにします。

```powershell
>npx create-nuxt-app sample-project
? Project name: sample-project
? Programming language: JavaScript
? Package manager: Npm
? UI framework: Bulma
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: ESLint, Prettier
? Testing framework: Jest
? Rendering mode: Single Page App
? Deployment target: Static (Static/JAMStack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
```

## テスト対象のコンポーネントを用意
今回は、componetsディレクトリ直下に以下のようなcomponentを用意しました。このcomponentをテスト対象としてみます。

```vuejs:Button.vue
<template>
  <div>
    <button
      :class="['button', colors || 'is-primary', size || 'is-normal']"
      :Disabled="disable"
      @click="clickHandler"
    >
      <slot>
        <span>{{ btnText }}</span>
      </slot>
    </button>
  </div>
</template>

<script>
export default {
  props: {
    btnText: String,
    colors: String,
    size: String,
    disable: Boolean,
  },
  methods: {
    clickHandler(event) {
      this.$emit('click', event)
    },
  },
}
</script>
```




## Storybookのセットアップ

### パッケージのインストール
まずはインストールします！

```powershell
npm i -D @storybook/vue @storybook/addon-actions 
```

### 環境設定
ルートディレクトリに　.storybookディレクトリを作成し、以下のファイルを作成、保存します。今回はaddon-actionsを利用してみるのでaddons.jsを用意しています。

```javascript:config.js
import { configure } from '@storybook/vue'
import 'bulma/css/bulma.css' // Nuxt上で読み込んでいるbulmaをstorybookでも使えるようにする

// ルートディレクトリ直下のstoriesディレクトリにある*.stories.jsファイルを読み込む
configure(require.context('../stories', true, /\.stories\.js$/), module)

```

```javascript:webpack.config.js
const path = require('path')
const rootPath = path.resolve(__dirname, '..') // プロジェクトのルートパスを指定する

module.exports = async ({ config }) => {
  mode = 'development'

  config.resolve.extensions = ['.js', '.vue', '.json']
  config.resolve.alias['~'] = rootPath // パス解決に必要
  config.resolve.alias['@'] = rootPath // パス解決に必要

  // 必要に同じてloaderを設定することもできる
  // 今回は不要なのでやらない

  return config
}
```

```javascript:addons.js
import '@storybook/addon-actions/register'
```

### storiesの追加
Button.vueを利用したstoriesを追加します。プロジェクトルートディレクトリの直下にstoriesディレクトリを作成し、以下のようなファイルを追加していきます。

```javascript:Button.stories.js
import { storiesOf } from '@storybook/vue'
import { action } from '@storybook/addon-actions'
import Button from '~/components/Button.vue'

storiesOf('atoms.Button', module).add('colors', () => ({
  components: { Button },
  template: `
      <div style="display: flex; flex-direction:column; align-items:center;">
        <Button colors="is-primary" @click="action" btn-text="is-primary" style="margin: 16px 0;"></Button>
        <Button colors="is-info" @click="action" btn-text="is-info" style="margin: 16px 0;"></Button>
        <Button colors="is-danger" @click="action" btn-text="is-danger" style="margin: 16px 0;"></Button>
      </div>
    `,
  methods: { action: action('クリックされました') },
}))
```

試しに、以下のようにstorybookの起動コマンドを追加して…

```json:package.json
  "scripts": {
    // 以下のコマンドを追加する
    "storybook": "start-storybook -p 6006"
  },
```

Storybookを起動してみます。

```powershell
npm run storybook
```

![コメント 2020-07-10 185219.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397131/a8e4aabb-d841-a507-80d2-a16ee7ee2c7d.png)

やり遂げました。

…ではなくて、ここからStoryshotsを追加していきます。。


## StoryshotsによるSnapShotテスト
Storybookの出力をスナップショット（画像ではなくテキスト情報です。念のため。。）として保存しておき、テスト時に変更があったかを自動で判定することができます。

私がプロジェクトを作成したときのJest（26.0.1）とStoryshots（5.3.19）の組み合わせでは"TypeError: require.requireActual is not a function"が出て動作しませんでした。[Jestを25に落とせば動く](https://stackoverflow.com/questions/61706963/typeerror-require-requireactual-is-not-a-function)とのことでしたので、該当する場合はJestのバージョンを25に指定してから以下の作業を進めてください。（他の解決策もあるかもです）


### パッケージのインストール
Storyshotsを利用するために以下のパッケージを読み込んでいきます。

```powershell
npm i -D @storybook/addon-storyshots babel-plugin-require-context-hook
```

### StorybookでimportしているCSSをMockする
Mockを追加しない場合、config.jsでBulmaをimportしている箇所で"SyntaxError: Invalid or unexpected token"と怒られてしまいます。以下に記載するものと別の手順などもありますので、詳細を確認したい方は[こちら](https://jestjs.io/docs/ja/webpack)からご確認ください。

#### Mockファイルの作成
まずは、ルートディレクトリに__mocks__ディレクトリを作成し、以下のファイルを追加していきます。

```javascript:styleMock.js
module.exports = {}
```

#### Jestの設定
以下のようにjest.config.jsを修正してcssにMockを割り当てます。

```javascript:jest.config.js
  moduleNameMapper: {
    // 以下の行をmoduleNameMapperに追加する
    '\\.(css)$': '<rootDir>/__mocks__/styleMock.js',
  },
```

### babelの設定
babelのプラグインであるbabel-plugin-require-context-hookをbabel.config.jsに設定します。今回使用したバージョンのStoryshotsでは、webpackのrequire.contextがサポートされていないということで、この設定をしないとStorybookのconfig.jsを読み込んだときに"TypeError: require.context is not a function"となってしまいます。（[こちらのissue](https://github.com/storybookjs/storybook/issues/4479)より）

```javascript:babel.config.js
  // 以下の設定を追加する
  env: {
    test: {
      plugins: ['require-context-hook'],
    },
  },
```

### スナップショットテストのテストファイルを作成
テストファイルを保存しているディレクトリにテストファイルを追加します。今回はルートディレクトリあるtestディレクトリに以下のファイルを追加しました。

```javascript:storyshots.spec.js
import path from 'path'
import initStoryshots from '@storybook/addon-storyshots'

initStoryshots({
  configPath: path.resolve(__dirname, '../.storybook'),
})
```

### テストを実行する
テストを実行してみます。

```powershell
npm run test
```

テストが成功すると、テストファイルを保存しているディレクトリに\_\_snapshots\_\_/storyshots.spec.js.snapが作成されます。このファイルは以下のように、Storybookで読みこんだButton.vueコンポーネントのスナップショットです。

```storyshots.spec.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Storyshots atoms.Button colors 1`] = `
<div
  style="display: flex; flex-direction: column; align-items: center;"
>
  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-primary is-normal"
    >
      <span>
        is-primary
      </span>
    </button>
  </div>
   
  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-info is-normal"
    >
      <span>
        is-info
      </span>
    </button>
  </div>
   
  <div
    style="margin: 16px 0px;"
  >
    <button
      class="button is-danger is-normal"
    >
      <span>
        is-danger
      </span>
    </button>
  </div>
</div>
`;
```

試しに、Button.vueのボタン内テキストを変更してみると、変更が検知されてテストが失敗します。

```テスト結果の一部抜き出し
  <button
    class="button is-info is-normal"
  >
    <span>
-     is-primary
+     変更した文字列
    </span>
  </button>

```

## StoryshotsによるVisual Regressionテスト
Storybookがビルドした結果を画像として保存しておき、テスト時に変更があったかを自動で判定することができます。

### パッケージのインストール
必要なパッケージをインストールします。

```powershell
npm i -D @storybook/addon-storyshots-puppeteer puppeteer
```

### Storybookのビルド
以下のようにpackage.jsonにビルドコマンドを追加してそのコマンドを叩くなどしてStorybookをビルドします。

```json:package.json
  "scripts": {
    // 以下のコマンドを追加する
    "build-storybook": "build-storybook"
  },
```

```powershell
npm run build-storybook
```

### Visual Regressionテストファイルの追加
テストファイルを保存しているディレクトリにテストファイルを追加します。今回はルートディレクトリあるtestディレクトリに以下のファイルを追加しました。

```javascript:image.spec.js
import path from 'path'
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'

initStoryshots({
  suite: 'Image storyshots',
  test: imageSnapshot({
    storybookUrl: `file://${path.resolve(__dirname, '..', 'storybook-static')}`, // Storybookをビルド出力先を指定
  }),
  configPath: path.resolve(__dirname, '..', '.storybook'),
})
```

### テストを実行する
テストを実行してみます。

```powershell
npm run test
```

テストが成功すると、テストファイルを保存しているディレクトリに\_\_image_snapshots\_\_ディレクトリが作成され、その中に画像ファイルが保存されています。このファイルは以下のように、Storybookで読みこんだButton.vueコンポーネントの画像スナップショットです。こちらは画像で比較を行い、画素に変更があればテスト失敗となります。

![コメント 2020-07-11 071741.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/397131/8008bbeb-515e-6384-822c-f91fac34797f.png)

## おわりに
何度かエラーに出くわしながらも、調べながら動作させることができました。今後、Storybook周りがアップデートされたらもっと簡単に導入できるようになるのかな🤔？？

運用にあたっての追加設定やテストの自動化など、いくつか調べていこうと思います。