背景
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
を使用する
- 2017年6月26日時点最新の2.3.0を使用すると
- vue: 2.3.4
- vuex: 2.3.1
- node: v8.1.2
- avoriaz: 2.2.0
- 本記事は、本家ドキュメントのUsing vuexを参考にしていますが、一部本記事用に調整しています。
- 今回のサンプルコードはこちらに全てアップロードしています。
Actionsのモック
Vuexのアクションを呼ぶ以下のコードを例にします。
ドキュメントによると、本テストの目的は
- アクションが何をするのか
- ストアがどんな形式か
を気にしません。私達が知りたいのは、イベントが必要な値で発火されたか否かのみです。
<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.
が表示されたので、こちらを参考に、'babel-polyfill'をimportしました。
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()
でスタブ化し、それを保持したストアを作成します。
describe('Actions.vue', () => {
describe('Mocking Actions', () => {
let actions;
let store;
beforeEach(() => {
actions = {
actionClick: sinon.stub(),
actionInput: sinon.stub(),
}
store = new Vuex.Store({
state: {},
actions
})
})
})
})
テストコード
今回の基本となるテストケースなので、一行ずつ確認していきます。
const wrapper = mount(Actions, { store })
- 第二引数に先ほど作成した
store
を渡す。これでActions.vue内のthis.$store
がモックのストアに置き換わる
const input = wrapper.find('input')[0]
- inputタグのDOMラッパーを取得
input.element.value = 'cat'
-
cat
という文字列を挿入する。しかしこの時点ではinput
イベントは発火しない。
input.trigger('input')
-
【重要】 avoriazの
trigger
を利用して、input
イベントを発火する。これにより、キーボードで文字列を入力した体になる。
expect(actions.actionInput.calledOnce).to.be.eql(true)
- テストコードでは
cat
という文字列が入力されれば、$storeのactionInput
アクションが呼ばれるので、それが一回呼ばれるか検証する。
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)
})
上記を参考に、以下テストケースも実装可能となります。
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のアクションを呼ぶ以下のコードを例にします。
ドキュメントによると、gettersが何を返すかは気にせずに、そのgettersの値を利用して、正しくレンダリングされたかを検証します。
<template>
<div>
<p v-if="animal">{{animal}}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default{
computed: mapGetters([
'animal'
]),
};
</script>
準備 : ストアのモックを作成
ここではgettersのモックを作成し、ストアに保持させます。
describe('Getters.vue', () => {
describe('Mocking Getters', () => {
let getters;
let store;
beforeEach(() => {
getters = {
animal: () => 'cat'
}
store = new Vuex.Store({
getters
})
})
})
})
テストコード
Gettersのテストコードはシンプルです。ストアのモックオブジェクトを渡し、指定の文字列がレンダリングされているかを検証します。
it('Pタグをレンダリングして、文字列が表示されている', () => {
const wrapper = mount(Getters, { store })
const p = wrapper.find('p')[0]
expect(p.text()).to.be.eql(getters.animal())
})
Modulesのモック
カウントアップを処理するカウンターモジュールを利用した場合を例にします。
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)
})
<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>
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が正しく処理されたかのテストを書くことができます。
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のメソッドが呼ばれたか検証できます。
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の値が利用されているか検証できます。
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を利用したコンポーネントのテストをする方法が分かりました。しかし前回と今回共に、基本的な使い方を中心にまとめただけで、avoriazを使用すると何が便利になるのか、コードがどう変わるのか分かりづらいです。次回は、avoriazの有り無しでコードがどう変わるのか検証したいと思います。