今天这篇文章主要想介绍两个重点:
下面是一个使用了 Vuex 的元件的简单范例,在元件中透过 count getter 函式取得 count 的值并渲染在 p 标签上,以及在 button 上挂载可以改变 count 值的 increment mutations。
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0
},
getters: {
count: state => state.count
},
mutations: {
increment (state) {
state.count += 1
}
}
})
import { computed } from 'vue'
import { useStore } from 'vuex'
const Component = {
template: `
<div>
<button data-test="increment" @click="increment" />
<p data-test="count">Count: {{ count }}</p>
</div>
`,
setup () {
const store = useStore()
const count = computed(() => store.getters['count'])
const increment = () => {
store.commit('increment')
}
return {
count,
increment
}
}
}
为了测试这个元件和 Vuex 是否正常交互运作,我们会点击 button 并断言 count 的值会从 0 变成 1增加。所以藉着我们这几天所分享的内容,你可能会这麽写
test('after clicked, value of count will become 0 to 1', async () => {
const wrapper = mount(Component)
expect(wrapper.html()).toContain('Count: 0')
await wrapper.get('[data-test="increment"]').trigger('click')
expect(wrapper.html()).toContain('Count: 1')
})
不过,你就会马上看到 TypeError: Cannot read property 'getters' of undefined
的错误讯息
这是因为我们仅在 main.js 中透过 app.use 安装 Vuex 作为 Vue plugin
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App).use(store).mount('#app')
但我们现在为了对元件进行测试而将其独立抽出引入,所以这时候元件中自然无法正常使用 Vuex,也因此我们需要用 Vue Test Utils 提供的 global.plugins 来在 mount 或是 shallowMount 的元件中安装 Vuex plugin。
import store from '@/store'
test('After clicked, value of count will become 0 to 1', async () => {
const wrapper = mount(Component, {
global: {
plugins: [store]
}
})
expect(wrapper.html()).toContain('Count: 0')
await wrapper.get('[data-test="increment"]').trigger('click')
expect(wrapper.html()).toContain('Count: 1')
})
正常来讲,在进行单元测试时,每一次的测试应该是彼此独立的,所以也不应该会因为测试案例的顺序而造成错误,但因为现在元件有和 Vuex 交互作用,又因为 Vuex 的集中式 (centralized) 状态管理的特性,所以造成可能会因为顺序的问题而导致错误。
什麽意思呢? 我们来看一下下面的程序码。
// pass
import store from '@/store'
describe('Testing Component with Vuex', () => {
test('The initial value of count is 0', async () => {
const wrapper = mount(Component, {
global: {
plugins: [store]
}
})
// current: state: { count: 0 }
expect(wrapper.html()).toContain('Count: 0')
})
test('After clicked, value of count will become 0 to 1', async () => {
const wrapper = mount(Component, {
global: {
plugins: [store]
}
})
// current: state: { count: 0 }
expect(wrapper.html()).toContain('Count: 0')
await wrapper.get('[data-test="increment"]').trigger('click')
// current: state: { count: 1 }
expect(wrapper.html()).toContain('Count: 1')
})
})
// fail
import store from '@/store'
describe('Testing Component with Vuex', () => {
test('After clicked, value of count will become 0 to 1', async () => {
const wrapper = mount(Component, {
global: {
plugins: [store]
}
})
// current: state: { count: 0 }
expect(wrapper.html()).toContain('Count: 0')
await wrapper.get('[data-test="increment"]').trigger('click')
// current: state: { count: 1 }
expect(wrapper.html()).toContain('Count: 1')
})
test('The initial value of count is 0', async () => {
const wrapper = mount(Component, {
global: {
plugins: [store]
}
})
// current: state: { count: 1 }
expect(wrapper.html()).toContain('Count: 0')
})
})
上下两段的程序码,只差在两个测试案例的顺序不同,不过上面的程序码会通过测试,而下面的程序码不会通过测试。
这是因为每一个测试运行时重新生成的只有元件本身,而现在 count 是储存在 Vuex 中,也因此上一个测试对 Vuex 的操作会影响到下一个测试 (可以从 current: state: { count: x }
的注解观察到 count 的变化。)
为了避免这个问题,我们来稍微改变一下 Vuex 的写法,我们用一个函式来包装 createStore 并且可以传递一个参数来当作 state 的初始值,这样的改动也会让我们在测试上以及开发上都有更高的弹性与使用。
import { createStore } from 'vuex'
const createVuexStore = (initialState) => {
const state = Object.assign({
count: 0
}, initialState)
return createStore({
state,
getters: {
count: state => state.count
},
mutations: {
increment (state) {
state.count += 1
}
}
})
}
export default createVuexStore()
export { createVuexStore }
import { createVuexStore } from '@/store'
describe('Vuex', () => {
test('After clicked, value of count will become 0 to 1', async () => {
const wrapper = mount(Component, {
global: {
plugins: [createVuexStore()]
}
})
expect(wrapper.html()).toContain('Count: 0')
await wrapper.get('[data-test="increment"]').trigger('click')
expect(wrapper.html()).toContain('Count: 1')
})
test('The initial value of count is 10', async () => {
const wrapper = mount(Component, {
global: {
plugins: [createVuexStore({ count: 10 })]
}
})
expect(wrapper.html()).toContain('Count: 10')
})
})
如果你想要为你的 Vuex 作单元测试也可以,因为 Vuex 就只是普通的 JavaScript,这完全和一般的单元测试没两样。
import { createVuexStore } from '@/store'
describe('Testing Vuex in Isolation', () => {
test('increment: 0 -> 1', () => {
const store = createVuexStore()
store.commit('increment')
expect(store.getters['count']).toBe(1)
})
test('increment: 10 -> 11', () => {
const store = createVuexStore({ count: 10 })
store.commit('increment')
expect(store.getters['count']).toBe(11)
})
})
今天的分享就到这边,如果大家对我分享的内容有兴趣欢迎点击追踪 & 订阅系列文章,如果对内容有任何疑问,或是文章内容有错误,都非常欢迎留言讨论或指教的!
明天要来分享的是 Vue3 E2E Testing 的主题了,那我们明天见!
<<: Day22 Android - RxJava(Observer+Observable)
>>: TailwindCSS 从零开始 - 价目表卡片实战 - 进阶卡片样式
本篇提到的故事是发生在我跟教授 B 签完指导教授确认单到发生意外之间。 进入正题 昨天有提到,B 教...
前二篇解释了 GUI Design 阶段的重点,也提到此时花费设计师的工时相当可观。功欲善其事,必先...
Review 由於 State 原本可以一篇写完的,被我拖成四篇的关系,所以来回顾一下,哈哈哈哈哈哈...
=== 书接上回,[Day 27] Edge Impulse + BLE Sense实现影像分类(上...
在上一篇中提到了如何建立与使用一个 Service,也大概介绍了什麽是 Dependency Inj...