5
4

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.

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

Last updated at Posted at 2019-12-06

Part1 https://qiita.com/senku/items/07c3e2859ac90c03867a
Part2 ここ
Part3 https://qiita.com/senku/items/08d547eda2c6ff818108

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

Summary

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

Vuexとのたたかい

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

$ vue add vuex

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

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を使えるようにしてみましょう。

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

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

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

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

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で渡すようにしておきます。

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

gettersのモック

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

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

$ npm install --save-dev lodash

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

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で使います。

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

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

actionsのモック

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

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.vuecreated()で呼ばれていたtest/setFooアクションをモックして、quxをセットしてみます。

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

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

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()のタイミングがよさげ。

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

つづく

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?