VuexのActions, Getters, Modulesを利用したコンポーネントをavoriazでテストする

## 背景
[Vuex](https://github.com/vuejs/vuex)は、Vue.js用の状態管理ライブラリです。中規模から大規模のSPAアプリケーションの構築する際に、Vuexの利用が勧められています。その為開発の現場で、Vuexを利用する機会も多いと思います。

今回はVuexのActions, Getters, Modulesを利用したコンポーネントを、avoriazでテストする方法をまとめます。

## 前提
* 以下ライブラリを利用します。
    * avoriaz: 2.2.0
        * 2017年6月26日時点最新の2.3.0を使用すると`trigger`メソッドが想定通りの挙動にならないので、`2.2.0`を使用する
    * vue: 2.3.4
    * vuex: 2.3.1
    * node: v8.1.2
* 本記事は、[本家ドキュメントのUsing vuex](https://eddyerburgh.gitbooks.io/avoriaz/content/guides/using-with-vuex.html)を参考にしていますが、一部本記事用に調整しています。
* 今回のサンプルコードは[こちら](https://github.com/hypermkt/hypermkt/tree/master/javascript/vuejs-avoriaz-vuex)に全てアップロードしています。

## Actionsのモック

Vuexのアクションを呼ぶ以下のコードを例にします。

[ドキュメント](https://eddyerburgh.gitbooks.io/avoriaz/content/guides/using-with-vuex.html)によると、本テストの目的は

* アクションが何をするのか
* ストアがどんな形式か

を気にしません。私達が知りたいのは、イベントが必要な値で発火されたか否かのみです。

```html:Actions.vue
<template>
    <div class="text-align-center">
        <input type="text" v-model="animal_name" @input="actionInput" />
        <button @click="actionClick()">Click</button>
    </div>
</template>

<script>
  import { mapActions } from 'vuex';

  export default{
    data () {
      return {
        animal_name: ''
      }
    },
    methods: {
      ...mapActions([
        'actionClick',
      ]),
      actionInput() {
        if (this.animal_name === 'cat') {
          this.$store.dispatch('actionInput', this.animal_name);
        }
      },
    },
  };
</script>
```

### 準備① : 必要ライブラリのインポート

* テストファイルの冒頭で各種ライブラリを読み込みます。
* Vuexの読み込むと `Error: [vuex] vuex requires a Promise polyfill in this browser.`が表示されたので、[こちら](https://github.com/vuejs-templates/webpack/issues/260#issuecomment-246801767)を参考に、'babel-polyfill'をimportしました。

```js:Actions.spec.js
import Vue from 'vue'
import Vuex from 'vuex'
import { mount } from 'avoriaz'
import sinon from 'sinon'
import 'babel-polyfill' // Error: [vuex] vuex requires a Promise polyfill in this browser.対策
import Actions from '../../src/components/Actions.vue'

Vue.use(Vuex)

```

### 準備② : ストアのモックを作成

コンポーネントをマウントする際に、モックしたストアをVuexに渡す必要があります。以下では、そのモックを準備しています。Vuexのアクションは`sinon.stub()`でスタブ化し、それを保持したストアを作成します。

```js:Actions.spec.js
describe('Actions.vue', () => {
  describe('Mocking Actions', () => {
    let actions;
    let store;

    beforeEach(() => {
      actions = {
        actionClick: sinon.stub(),
        actionInput: sinon.stub(),
      }
      store = new Vuex.Store({
        state: {},
        actions
      })
    })
　　　　})
})
```

### テストコード
今回の基本となるテストケースなので、一行ずつ確認していきます。

1. `const wrapper = mount(Actions, { store })`
  * 第二引数に先ほど作成した`store`を渡す。これでActions.vue内の`this.$store`がモックのストアに置き換わる
1. `const input = wrapper.find('input')[0]`
  * inputタグのDOMラッパーを取得
1. `input.element.value = 'cat'`
  * `cat`という文字列を挿入する。**しかしこの時点では`input`イベントは発火しない。**
1. `input.trigger('input')`
  * **【重要】** avoriazの`trigger`を利用して、`input`イベントを発火する。これにより、キーボードで文字列を入力した体になる。
1. `expect(actions.actionInput.calledOnce).to.be.eql(true)`
  * テストコードでは`cat`という文字列が入力されれば、$storeの`actionInput`アクションが呼ばれるので、それが一回呼ばれるか検証する。

```js:Actions.spec.js
    it('inputタグに「cat」が入力されinputイベントが発火したら、store アクションのactionInputが呼ばれる', () => {
      const wrapper = mount(Actions, { store })
      const input = wrapper.find('input')[0]
      input.element.value = 'cat'
      input.trigger('input')
      expect(actions.actionInput.calledOnce).to.be.eql(true)
    })
```

上記を参考に、以下テストケースも実装可能となります。

```js:Actions.spec.js
    it('inputタグに「dog」が入力されinputイベントが発火したら、store アクションのactionInputが呼ばれない', () => {
      const wrapper = mount(Actions, { store })
      const input = wrapper.find('input')[0]
      input.element.value = 'dog'
      input.trigger('input')
      expect(actions.actionInput.calledOnce).to.be.eql(false)
    })

    it('ボタンがクリックされたら、actionClickが呼ばれる', () => {
      const wrapper = mount(Actions, { store })
      wrapper.find('button')[0].trigger('click')
      expect(actions.actionClick.calledOnce).to.be.eql(true)
    })
  })
})
```

## Gettersのモック

Vuexのアクションを呼ぶ以下のコードを例にします。

[ドキュメント](https://eddyerburgh.gitbooks.io/avoriaz/content/guides/using-with-vuex.html)によると、gettersが何を返すかは気にせずに、そのgettersの値を利用して、正しくレンダリングされたかを検証します。

```html:Getters.js
<template>
    <div>
        <p v-if="animal">{{animal}}</p>
    </div>
</template>

<script>
  import { mapGetters } from 'vuex';

  export default{
    computed: mapGetters([
      'animal'
    ]),
  };
</script>
```

### 準備 : ストアのモックを作成
ここではgettersのモックを作成し、ストアに保持させます。

```js:Getters.spec.js
describe('Getters.vue', () => {
  describe('Mocking Getters', () => {
    let getters;
    let store;

    beforeEach(() => {
      getters = {
        animal: () => 'cat'
      }
      store = new Vuex.Store({
        getters
      })
    })
  })
})

```

### テストコード
Gettersのテストコードはシンプルです。ストアのモックオブジェクトを渡し、指定の文字列がレンダリングされているかを検証します。

```js:Getters.spec.js
    it('Pタグをレンダリングして、文字列が表示されている', () => {
      const wrapper = mount(Getters, { store })
      const p = wrapper.find('p')[0]
      expect(p.text()).to.be.eql(getters.animal())
    })
```

## Modulesのモック
カウントアップを処理するカウンターモジュールを利用した場合を例にします。

```js:src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({ // eslint-disable-line no-new
  el: '#app',
  store,
  render: (h) => h(App)
})
```

```html:src/components/Modules.vue
<template>
    <div>
        <h2>Counter</h2>
        <h3>Count: {{ current_count }}</h3>
        <button v-on:click="increment()">Count Up</button>
        <input id="number-input" v-model="input_count" type="text" @input="changeCount" />
    </div>
</template>

<script>
import { mapActions, mapGetters } from 'vuex'

export default {
  data() {
    return {
      input_count: ''
    }
  },
  computed: {
    ...mapGetters(['current_count'])
  },
  methods: {
    ...mapActions(['increment']),
    changeCount() {
      if (isFinite(this.input_count)) {
        this.$store.dispatch('input_number', this.input_count)
      }
    }
  }
}
</script>
```


```js:src/store/modules/counter.js
import * as types from '../mutation-types'

// 初期状態
const state = {
  count: 0
}

// ゲッター
const getters = {
  current_count: state => state.count
}

// アクション
const actions = {
  increment (context) {
    context.commit(types.INCREMENT)
  },
  input_number (context, count) {
    context.commit(types.INPUT_NUMBER, count)
  }
}

// ミューテーション
const mutations = {
  [types.INCREMENT] (state) {
    state.count++
  },
  [types.INPUT_NUMBER] (state, count) {
    state.count = count
  }
}

export default {
  state,
  actions,
  getters,
  mutations
}
```

### 準備
モジュールを利用している場合も、Actionsのメソッドをモック化するところは今までと同じです。1点他と違うのは、モジュール本体のgettersを渡します。その為の仮のstateを用意することで、gettersが正しく処理されたかのテストを書くことができます。

```js
describe('Modules.vue', () => {
  describe('Mocking with Modules', () => {
    let actions;
    let state;
    let store;

    beforeEach(() => {
      state = {
        count: 0
      }
      actions = {
        increment: sinon.stub(),
        input_number: sinon.stub(),
      }
      store = new Vuex.Store({
        state,
        actions,
        getters: counter.getters
      })
    })
  })
})
```

### テストコード
以下はActionsのメソッドが呼ばれたか検証できます。

```js:Modules.spec.js
    it('Count upボタンをクリックすると、incrementアクションが呼ばれる', () => {
      const wrapper = mount(Modules, { store })
      const button = wrapper.find('button')[0]
      button.trigger('click')
      expect(actions.increment.calledOnce).to.be.eql(true)
    })
```

gettersは、モジュール本体のを渡しているので、想定通りHTMLがレンダリングされ、stateの値が利用されているか検証できます。

```js:Modules.spec.js
    it('Count upボタンをクリックする前は、カウント0である', () => {
      const wrapper = mount(Modules, { store })
      const h3 = wrapper.find('h3')[0]
      expect(h3.text()).to.be.eql('Count: 0')
    })
```

## まとめ
`sinon.stub()`とavoriazの`.trigger`を利用することで、Vuexを利用したコンポーネントのテストをする方法が分かりました。しかし[前回](http://qiita.com/hypermkt/items/850bf20a8ad43c13fd05)と今回共に、基本的な使い方を中心にまとめただけで、avoriazを使用すると何が便利になるのか、コードがどう変わるのか分かりづらいです。次回は、avoriazの有り無しでコードがどう変わるのか検証したいと思います。

## 参考
* [using with vuex · Avoriaz](https://eddyerburgh.gitbooks.io/avoriaz/content/guides/using-with-vuex.html)
* [Vuex@2.0 Document](https://vuex.vuejs.org/ja/)
* [Vue\.js用テストライブラリ「avoriaz」入門 \- Qiita](http://qiita.com/hypermkt/items/850bf20a8ad43c13fd05)