はじめに
肥大化していく複雑さに対抗しうるはテストの存在。
Vuexにてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染むJest。
いざ書かん。
import store from '@pass/to/store'
describe('Test Vuex module', () => {
/**
* store内部では
* state = {
* items: []
* }
* で初期化されています。
*/
test('itemsでstate.itemsを初期化', () => {
const items = [
{ id: 1, name: 'hoge' }
]
store.commit('models/User/setItems', items)
expect(store.state.items).toEqual(items) //pass
})
test('state.itemに要素を追加', () => {
const item = { id: 1, name: 'hoge' }
store.commit('models/User/addItem', item)
expect(store.state.items.length).toBe(1) //failure, received 2
})
})
そして敗れる我がテスト。
上記のテストでは1つ目のテストは成功するものの、2つ目のテストでは配列の要素数が期待どおりではなく失敗しています。
Vuex自体の動作としては望ましいですが、stateの状態をテストスイート全体で追いかけるのは不便で面倒です。
(公式のチュートリアルではmutationやactionを個別でインポートし、stateをモックすることでテストを行っています。)
テストはそれ自体、書きやすくあってほしいものです。
そこで本記事ではVuexにまつわる諸々をモック化し、mutationやactionのテストが楽になるような方法を提案します。
本記事で使用したコードはここにあるので気になった方は見てみてください。
モック後のテストコードのイメージはこんな感じです。
describe('Test your Vuex module', () => {
test('mutationでstateを初期化', () => {
const items = [
{ id: 1, name: 'hoge' }
]
const store = new MockedVuexStore(state, mutations, actions)
store.commit('setItems', items)
expect(store.state.items).toEqual(items)
})
test('非同期なactionで要素を追加', () => {
const item = { id: 1, name: 'hoge' }
const store = new MockedVuexStore(state, mutations, actions)
store.dispatch('add', item).then(res => {
expect(store.state.items.length).toBe(1)
})
})
})
mutation、action、同期、非同期関わらず割と見通しの良いテストコードではないでしょうか。
#さっそく書いていきます。
Vuex.StoreクラスをモックしたものをここではMockedVuexStore
クラスとします。
MockedVuexStore
クラスに期待するは、
- テストケース毎に独立したStateを持てる
-
commit(mutationName, payload)
メソッドからmutationを発火させる -
dispatch(actionName, payload)
メソッドからactionを発火させる
てところでしょうか。
下記がクラス本文です。
export class MockedVuexStore {
constructor(state, mutations, actions) {
this.state = state
this.mutations = {
...mutations
}
this.actions = {
...actions
}
}
commit(type, payload = null) {
try {
this.mutations[type](this.state, payload)
} catch (e) {
if (!e instanceof TypeError) {
throw e
}
console.log(`unknown mutation "${type}" is called`)
}
}
dispatch(type, payload = null) {
try {
return this.actions[type](
{
commit: (type, payload) => this.commit(type, payload)
},
payload
)
} catch (e) {
if (!e instanceof TypeError) {
throw e
}
console.log(`unknown action "${type}" is called`)
}
}
}
コンストラクタの引数としてstate, mutations, actionsを外部から受け取っております。
- stateは、テストケース内で定義されたもの
- mutationsは、テスト対象モジュールのmutation
- actionsは、テスト対象モジュールのaction
です。
dispatch
メソッド内の
return this.actions[type](
{
commit: (type, payload) => this.commit(type, payload)
},
payload
)
ですが、呼び出し先のactionにおいてgetters
,dispatch
,state
が使用される場合は、それらについて適宜モックする必要があります。(今回は言及しません。)
ともあれこれで私たちはシンプルにモック化されたVuex.Storeを手に入れることができました。
#テスト書きます。
肥大化していく複雑さに対抗しうるはテストの存在。
Vuexにてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染むJest。
モックされるはVuex.Store
いざ書かん。
import $axios from '../store/HttpClient'
import { mutations, actions } from '../store/Models/User'
import { MockedVuexStore } from './mocks'
/* axiosをモック */
jest.mock('../store/HttpClient')
describe('Test your Vuex module', () => {
test('mutationでstateを初期化', () => {
const state = {
items: []
}
const items = [
{ id: 1, name: 'hoge' }
]
const store = new MockedVuexStore(state, mutations, actions)
store.commit('setItems', items)
expect(store.state.items).toEqual(items)
})
test('非同期なactionで要素を追加', () => {
const state = {
items: []
}
const item = { id: 1, name: 'hoge' }
/* axiosのメソッドをモック */
$axios.post.mockResolvedValue({
data: item,
status: 200
})
const store = new MockedVuexStore(state, mutations, actions)
store.dispatch('add', item).then(res => {
expect(store.state.items.length).toBe(1)
})
})
})
そして成功する我がテスト。
なお、外部のAPIを叩くのに使っている(後述のUserストア内で使用しています。)axiosはJestのMock Functions でモックしています。
import $axios from '../store/HttpClient'
/* axiosをモック */
jest.mock('../store/HttpClient')
/* axiosのpostメソッドをモック */
$axios.post.mockResolvedValue({
data: item,
status: 200
})
###その他もろもろ
ディレクトリ構成
.
├── babel.config.js
├── jest.config.js
├── node_modules
├── package.json
├── store
│ ├── HttpClient.js
│ ├── Models
│ │ ├── User.js
│ │ └── index.js
│ └── index.js
├── tests
│ ├── Failure.test.js
│ ├── Success.test.js
│ ├── User.test.js
│ └── mocks.js
└── yarn.lock
テスト対象のUserストア
import $axios from '../HttpClient'
const state = {
items: []
}
const getters = {}
export const mutations = {
setItems(state, data) {
state.items = data
},
addItem(state, data) {
state.items.push(data)
},
updateItem(state, data) {
const index = state.items.findIndex(item => item.id == data.id)
Object.assign(state.items[index], data)
},
deleteItem(state, id) {
const index = state.items.findIndex(item => item.id == id)
state.items.splice(index, 1)
}
}
export const actions = {
async fetch(context) {
const response = await $axios.get('/api/users')
context.commit('setItems', response.data)
return 'OK'
},
async add(context, data) {
const response = await $axios.post(`/api/users/add`, data)
context.commit('addItem', response.data)
return 'OK'
},
async edit(context, data) {
const response = await $axios.post(`/api/users/${data.id}/edit`, data)
context.commit('updateItem', response.data)
return 'OK'
},
async delete(context, id) {
await $axios.delete(`/api/users/${id}/delete`)
context.commit('deleteItem', id)
return false
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
HTTP通信用のaxiosインスタンス
import axios from 'axios'
let baseURL = '/'
const $axios = axios.create({
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
baseURL: baseURL
})
export default $axios
#終わりに
なるべくいつものVuexに近い形でテストが実行できるようにモックしてみました。
今後の課題としては、
- {root: true}なcommitやdispatchへの対応
- TypeScriptによる型安全な開発
あたりを頑張ればより複雑なケースに対しても対応できるのかなあとと思います。
本記事で使用したコードはここにあります。
ありがとうございました!