今天這篇文章主要想介紹兩個重點:
下面是一個使用了 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 的主題了,那我們明天見!