ゼロからVue.jsでビジュアルリグレッションテストするまでpart2/3

Part1 [https://qiita.com/senku/items/07c3e2859ac90c03867a](https://qiita.com/senku/private/07c3e2859ac90c03867a)
Part2 ここ
Part3 [https://qiita.com/senku/items/08d547eda2c6ff818108](https://qiita.com/senku/private/08d547eda2c6ff818108)

前回はVuex以外の主要なライブラリをStorybookに対応させる手順を紹介しました。
今回はVuexの話をします。ちょっと大変。

## Summary

- `Vuex.Store`の引数のObjectをexportしておいて
- モックに書き換えて
- Storybookに使わせる

## Vuexとのたたかい

とりあえず`vue add vuex`で入れます。

```bash
$ vue add vuex
```

`src/store/index.js`にVuex Storeの定義が書かれています。
`Vuex.Store()`の引数をexportしつつ、適当なストアにまるっと書き換えます。namespaceにも対応しておくよ。

```js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export const store = {
  modules: {
    test: {
      namespaced: true,
      state: {
        name: ''
      },
      mutations: {
        setName: (state, payload) => {
          state.name = payload
        }
      },
      actions: {
        async setFoo({ commit }) {
          commit('setName', 'Foo')
        },
        async setBar({ commit }) {
          commit('setName', 'Bar')
        }
      },
      getters: {
        name: state => state.name 
      }
    }
  }
}

export default new Vuex.Store(store)
```

`src/views/Home.vue` あたりでVuexを使えるようにしてみましょう。

```diff:src/views/Home.vue
diff --git a/src/views/Home.vue b/src/views/Home.vue
index fc2e940..807ed24 100644
--- a/src/views/Home.vue
+++ b/src/views/Home.vue
@@ -1,11 +1,13 @@
 <template>
   <div class="home">
+    <p>{{ name }}</p>
     <img alt="Vue logo" src="../assets/logo.png">
     <HelloWorld msg="Welcome to Your Vue.js App"/>
   </div>
 </template>

 <script>
+import { mapGetters } from 'vuex'
 // @ is an alias to /src
 import HelloWorld from '@/components/HelloWorld.vue'

@@ -13,6 +15,12 @@ export default {
   name: 'home',
   components: {
     HelloWorld
+  },
+  computed: {
+    ...mapGetters('test', ['name']),
+  },
+  created() {
+    this.$store.dispatch('test/setFoo')
   }
 }
 </script>
```

`npm run serve`で確認すると`Foo`が表示されます。バッチリ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/506b7f4d-6a80-b130-b5f3-f66d12da8213.png)

Home用のStoryも作っておきましょう。いつものパターン。

```js:src/stories/Home.stories.js
import { storiesOf } from "@storybook/vue";
import defaultDecorator from "@/stories/defaultDecorator";
import Home from "@/views/Home.vue";

storiesOf("Home", module)
  .addDecorator(defaultDecorator)
  .add("test", () => {
    return {
      components: { Home },
      template: `
      <home />
      `
    };
  });
```

### Vuex × Storybook

いつものようにこのStoryもタダでは動きません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/6e15ddb5-a2c4-0b94-5ea8-4c73788dd68c.png)

まずはVue Routerと同じように`config/storybook/config.js`で`Vue.use()`します。

```diff:config/storybook/config.js
diff --git a/config/storybook/config.js b/config/storybook/config.js
index d8dcd31..db6a520 100644
--- a/config/storybook/config.js
+++ b/config/storybook/config.js
@@ -2,8 +2,10 @@
 import { configure } from '@storybook/vue'
 import Vue from 'vue'
 import Router from 'vue-router'
+import Vuex from 'vuex'

 Vue.use(Router)
+Vue.use(Vuex)

 const req = require.context('../../src/stories', true, /.stories.js$/)
```

decoratorでStoreを渡してもいいのですが、Storyごとに異なるモックデータを使いたいケースを考慮して、Storyで渡すようにしておきます。

```diff:src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 2902105..802e067 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,12 +1,16 @@
 import { storiesOf } from "@storybook/vue";
 import defaultDecorator from "@/stories/defaultDecorator";
 import Home from "@/views/Home.vue";
+import Vuex from "vuex";
+import { store } from "@/store/index.js";

 storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
+    const mockStore = new Vuex.Store(store);
     return {
       components: { Home },
+      store: mockStore,
       template: `
       <home />
       `
```

Storyでもガッツリ`Foo`が表示されるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/0643507f-38b1-fa01-efff-65eff458ff68.png)

### gettersのモック

getters で取得する値をストーリーによって変えたいことがありますよね。モックで対応しましょう。

Deep copyを利用したいので、とりあえずlodashを入れます。Deep copyの実装面倒だからね。

```bash
$ npm install --save-dev lodash
```

Vuex Storeをモックするための独自のクラスをひとつ作って、gettersを上書きするためのユーティリティ関数を作ってしまいます。後のことも考えて適当に共通化しておきます。
また、オリジナルのstoreオブジェクトを改変すると他のStoryで困る可能性があるので、`cloneDeep`で複製したObjectを使います。
ここがこの記事の見どころですよ。それにしてもコードフォーマットバラバラだな。

```js:src/stories/mockVuex.js
import cloneDeep from "lodash/cloneDeep";
import Vuex from "vuex";
import { store as originalStore } from "@/store/index.js";

export default class mockVuex {
  constructor() {
    // モックするVuex StoreをDeep copy
    this.mockStore = cloneDeep(originalStore);
  }

  /**
   * Vuex Storeを生成して返却する
   * 
   * @return {Vuex.Store} store
   */
  getMockStore() {
    return new Vuex.Store(this.mockStore);
  }

  /**
   * mockStoreのプロパティを上書きする
   *
   * @param {string} field actions/getters
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockProperty(field, nameSpace, propertyName, newFunction) {
    let modules = this.mockStore.modules

    // nameSpace に従ってモジュールの階層を掘る
    const diggingNameSpaces = nameSpace.split('/')
    const lastNameSpace = diggingNameSpaces.pop()
    for (const diggingNameSpace of diggingNameSpaces) {
      modules = modules[diggingNameSpace].modules
    }
    modules[lastNameSpace][field][propertyName] = newFunction
  }

  /**
   * getters を上書きする
   *
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockGetters(nameSpace, propertyName, newFunction) {
    this.mockProperty("getters", nameSpace, propertyName, newFunction);
  }
}
```

Storyを修正します。Vuexは`mockVuex`に隠蔽したので参照不要になりました。
`mockGetters()`関数を使って、gettersが返す値を変更します。
その後`getMockStore()`で取得したVuex StoreをStoryで使います。

```diff:src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index 802e067..d1e5e74 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -1,16 +1,16 @@
 import { storiesOf } from "@storybook/vue";
 import defaultDecorator from "@/stories/defaultDecorator";
+import mockVuex from "@/stories/mockVuex";
 import Home from "@/views/Home.vue";
-import Vuex from "vuex";
-import { store } from "@/store/index.js";

 storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
-    const mockStore = new Vuex.Store(store);
+    const vuex = new mockVuex();
+    vuex.mockGetters('test', 'name', () => 'Baz')
     return {
       components: { Home },
-      store: mockStore,
+      store: vuex.getMockStore(),
       template: `
       <home />
       `
```

`test/name`のgettersがモックされ、`Baz`が表示されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/27bdcfbe-d3f2-01d5-d9c4-106362ccf5a3.png)

実運用では、モック用のデータを別に用意してimportするのがいいと思います。モックサーバとかに使ってるデータがあればそれで。

### actionsのモック

actionsもモックしたいですよね。
`src/stories/mockVuex`に`mockActions()`関数を追加します。共通化してるのでただのラッパー関数です。

```js:src/stories/mockVuex.jsに追加
  /**
   * actions を上書きする
   *
   * @param {string} nameSpace 'foo/bar/baz' 形式
   * @param {string} propertyName
   * @param {function} newFunction
   */
  mockActions(nameSpace, propertyName, newFunction) {
    this.mockProperty('actions', nameSpace, propertyName, newFunction)
  }
```

Storyを編集して、`Home.vue`の`created()`で呼ばれていた`test/setFoo`アクションをモックして、`qux`をセットしてみます。

```diff:src/stories/Home.stories.js
diff --git a/src/stories/Home.stories.js b/src/stories/Home.stories.js
index d1e5e74..27a8fef 100644
--- a/src/stories/Home.stories.js
+++ b/src/stories/Home.stories.js
@@ -7,7 +7,7 @@ storiesOf("Home", module)
   .addDecorator(defaultDecorator)
   .add("test", () => {
     const vuex = new mockVuex();
-    vuex.mockGetters('test', 'name', () => 'Baz')
+    vuex.mockActions('test', 'setFoo', ({state}) => state.name = "qux")
     return {
       components: { Home },
       store: vuex.getMockStore(),
```

ガッツリ`qux`が表示されます。やったね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543568/5849ab04-e191-806b-9e33-3cc6a946fa12.png)

画面表示に必要なデータをモックするのはgettersのモックでだいたいなんとかなるので、actionsをモックするケースはHTTP通信をしたりStorybookで動作しない副作用を制御したい場合がほとんどだと思います。
その場合、actionsはmockVuexのコンストラクタでまとめて無効化してしまうのが楽ちんです。

```js:src/stories/mockVuex.jsを編集
  constructor() {
    // モックするVuex StoreをDeep copy
    this.mockStore = cloneDeep(originalStore);

    // 以下使わないActionを無効化
    const NOOP = () => {}
    this.mockActions('test', 'setBar', NOOP)
  }
```

もうVuexのモックも怖くないですね。
明日からVue.jsコンポーネントのStoryを量産できるはずです。

## おまけ：コンポーネントのdataを操作する

Storyから`$refs`経由でコンポーネントにアクセスすればいいです。`mounted()`のタイミングがよさげ。

```js
  .add("edit name", () => {
    return {
      components: { Home },
      template: `
      <home ref="foo"/>
      `,
      mounted() {
        this.$refs.foo.bar = 'baz' // Homeコンポーネントのbarに'baz'をセット
      }
    };
  });
```

[つづく](https://qiita.com/senku/private/08d547eda2c6ff818108)