6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

試験的に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にあげているので、詳細を確認したいときはこちらにアクセスくださいm(__)m

Nuxtプロジェクトの作成

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

>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をテスト対象としてみます。

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のセットアップ

パッケージのインストール

まずはインストールします!

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

環境設定

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

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)

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
}
addons.js
import '@storybook/addon-actions/register'

storiesの追加

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

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の起動コマンドを追加して…

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

Storybookを起動してみます。

npm run storybook

コメント 2020-07-10 185219.png

やり遂げました。

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

StoryshotsによるSnapShotテスト

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

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

パッケージのインストール

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

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"と怒られてしまいます。以下に記載するものと別の手順などもありますので、詳細を確認したい方はこちらからご確認ください。

Mockファイルの作成

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

styleMock.js
module.exports = {}

Jestの設定

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

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より)

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

スナップショットテストのテストファイルを作成

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

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

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

テストを実行する

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

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がビルドした結果を画像として保存しておき、テスト時に変更があったかを自動で判定することができます。

パッケージのインストール

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

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

Storybookのビルド

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

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

Visual Regressionテストファイルの追加

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

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'),
})

テストを実行する

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

npm run test

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

コメント 2020-07-11 071741.png

おわりに

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

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

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?