在本教程中,我们将会看到如何用 Vue Test Utils 测试组件中的 Vuex,以及如何测试一个 Vuex store。
我们来看一些代码。
这是我们想要测试的组件。它会调用 Vuex action。
<template>
<div class="text-align-center">
<input type="text" @input="actionInputIfTrue" />
<button @click="actionClick()">Click</button>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default{
methods: {
...mapActions([
'actionClick'
]),
actionInputIfTrue: function actionInputIfTrue (event) {
const inputValue = event.target.value
if (inputValue === 'input') {
this.$store.dispatch('actionInput', { inputValue })
}
}
}
}
</script>
站在测试的角度,我们不关心这个 action 做了什么或者这个 store 是什么样子的。我们只需要知道这些 action 将会在适当的时机触发,以及它们触发时的预期值。
为了完成这个测试,我们需要在浅渲染组件时给 Vue 传递一个伪造的 store。
我们可以把 store 传递给一个 localVue
,而不是传递给基础的 Vue 构造函数。localVue
是一个独立作用域的 Vue 构造函数,我们可以对其进行改动而不会影响到全局的 Vue 构造函数。
我们来看看它的样子:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Actions.vue', () => {
let actions
let store
beforeEach(() => {
actions = {
actionClick: jest.fn(),
actionInput: jest.fn()
}
store = new Vuex.Store({
state: {},
actions
})
})
it('当事件的值是“input”时会 dispatch“actionInput”', () => {
const wrapper = shallowMount(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'input'
input.trigger('input')
expect(actions.actionInput).toHaveBeenCalled()
})
it('当事件的值不是“input”时不会 dispatch “actionInput”', () => {
const wrapper = shallowMount(Actions, { store, localVue })
const input = wrapper.find('input')
input.element.value = 'not input'
input.trigger('input')
expect(actions.actionInput).not.toHaveBeenCalled()
})
it('当按钮被点击时候调用“actionClick”的 action', () => {
const wrapper = shallowMount(Actions, { store, localVue })
wrapper.find('button').trigger('click')
expect(actions.actionClick).toHaveBeenCalled()
})
})
这里发生了什么?首先我们用 localVue.use
方法告诉 Vue 使用 Vuex。这只是 Vue.use
的一个包裹器。
然后我们用 new Vuex.Store
伪造了一个 store 并填入假数据。我们只把它传递给 action,因为我们只关心这个。
该 action 是 Jest 伪造函数。这些伪造函数让我们去断言该 action 是否被调用。
然后我们可以在我们的测试中断言 action 存根是否如预期般被调用。
现在我们定义 store 的方式在你看来可能有点特别。
我们使用 beforeEach
来确认我们在每项测试之前已经拥有一个干净的 store。beforeEach
是一个 mocha 的钩子,会在每项测试之前被调用。我们在测试中会重新为 store 的变量赋值。如果我们没有这样做,伪造函数就需要被自动重置。它还需要我们改变测试中的 state,而不会影响后面的其它测试。
该测试中最重要的注意事项是:我们创建了一个伪造的 Vuex store 并将其传递给 Vue Test Utils。
好的,现在我们可以伪造 action 了,我们再来看看伪造 getter。
<template>
<div>
<p v-if="inputValue">{{inputValue}}</p>
<p v-if="clicks">{{clicks}}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default{
computed: mapGetters([
'clicks',
'inputValue'
])
}
</script>
这是一个非常简单的组件。它根据 getter clicks
和 inputValue
渲染结果。还是那句话,我们并不关注这些 getter 返回什么——只关注它们被正确的渲染。
让我们看看这个测试:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('Getters.vue', () => {
let getters
let store
beforeEach(() => {
getters = {
clicks: () => 2,
inputValue: () => 'input'
}
store = new Vuex.Store({
getters
})
})
it('在第一个 p 标签中渲染“state.inputValue”', () => {
const wrapper = shallowMount(Getters, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(getters.inputValue())
})
it('在第二个 p 标签中渲染“state.clicks”', () => {
const wrapper = shallowMount(Getters, { store, localVue })
const p = wrapper.findAll('p').at(1)
expect(p.text()).toBe(getters.clicks().toString())
})
})
这个测试和我们的 action 测试很相似。我们在每个测试运行之前创建了一个伪造的 store,在我们调用 shallowMount
的时候将其以一个选项传递进去,并断言我们伪造的 getter 的返回值被渲染。
这非常好,但是如果我们想要检查我们的 getter 是否返回了正确的 state 的部分该怎么办呢?
Module 对于将我们的 store 分隔成多个可管理的块来说非常有用。它们也暴露 getter。我们可以在测试中使用它们。
看看这个组件:
<template>
<div>
<button @click="moduleActionClick()">Click</button>
<p>{{moduleClicks}}</p>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex'
export default{
methods: {
...mapActions([
'moduleActionClick'
])
},
computed: mapGetters([
'moduleClicks'
])
}
</script>
简单的包含一个 action 和一个 getter 的组件。
其测试:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'
const localVue = createLocalVue()
localVue.use(Vuex)
describe('MyComponent.vue', () => {
let actions
let state
let store
beforeEach(() => {
state = {
clicks: 2
}
actions = {
moduleActionClick: jest.fn()
}
store = new Vuex.Store({
modules: {
myModule: {
state,
actions,
getters: myModule.getters
}
}
})
})
it('在点击按钮时调用 action“moduleActionClick”', () => {
const wrapper = shallowMount(MyComponent, { store, localVue })
const button = wrapper.find('button')
button.trigger('click')
expect(actions.moduleActionClick).toHaveBeenCalled()
})
it('在第一个 p 标签内渲染“state.inputValue”', () => {
const wrapper = shallowMount(MyComponent, { store, localVue })
const p = wrapper.find('p')
expect(p.text()).toBe(state.clicks.toString())
})
})
这里有两个测试 Vuex store 的方式。第一个方式是分别单元化测试 getter、mutation 和 action。第二个方式是创建一个 store 并针对其进行测试。我们接下来看看这两种方式如何。
为了弄清楚如果测试一个 Vuex store,我们会创建一个简单的计数器 store。该 store 会有一个 increment
mutation 和一个 evenOrOdd
getter。
// mutations.js
export default {
increment (state) {
state.count++
}
}
// getters.js
export default {
evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}
Getter、mutation 和 action 全部是 JavaScript 函数,所以我们可以不通过 Vue Test Utils 和 Vuex 测试它们。
分别测试 getter、mutation 和 action 的好处是你的单元测试是非常详细的。当它们失败时,你完全知道你代码的问题是什么。当然另外一方面你需要伪造诸如 commit
和 dispatch
的 Vuex 函数。这会导致在一些情况下你伪造错了东西,导致单元测试通过,生产环境的代码缺失败了。
我们会创建两个测试文件:mutations.spec.js
和 getters.spec.js
:
首先,我们测试名为 increment
的 mutation:
// mutations.spec.js
import mutations from './mutations'
test('increment increments state.count by 1', () => {
const state = {
count: 0
}
mutations.increment(state)
expect(state.count).toBe(1)
})
现在让我们测试 evenOrOdd
getter。我们可以通过创建一个伪造的 state
来测试它,带上 state
调用这个 getter 并检查它是否返回正确的结果。
// getters.spec.js
import getters from './getters'
test('evenOrOdd returns even if state.count is even', () => {
const state = {
count: 2
}
expect(getters.evenOrOdd(state)).toBe('even')
})
test('evenOrOdd returns odd if state.count is odd', () => {
const state = {
count: 1
}
expect(getters.evenOrOdd(state)).toBe('odd')
})
另一个测试 Vuex store 的方式就是使用 store 配置创建一个运行中的 store。
这样做的好处是我们不需要伪造任何 Vuex 函数。
另一方面当一个测试失败时,排查问题的难度会增加。
我们来写一个测试吧。当我们创建一个 store 时,我们会使用 localVue
来避免污染 Vue 的基础构造函数。该测试会使用 store-config.js
导出的配置创建一个 store:
// store-config.spec.js
import mutations from './mutations'
import getters from './getters'
export default {
state: {
count: 0
},
mutations,
getters
}
import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'
test('increments count value when increment is commited', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store(cloneDeep(storeConfig))
expect(store.state.count).toBe(0)
store.commit('increment')
expect(store.state.count).toBe(1)
})
test('updates evenOrOdd getter when increment is commited', () => {
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store(cloneDeep(storeConfig))
expect(store.getters.evenOrOdd).toBe('even')
store.commit('increment')
expect(store.getters.evenOrOdd).toBe('odd')
})
注意我们在创建一个 store 之前使用了 cloneDeep
来克隆 store 配置。这是因为 Vuex 会改变用来创建 store 的选项对象。为了确保我们能为每一个测试都提供一个干净的 store,我们需要克隆 storeConfig
对象。