iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0
Modern Web

前端工程師在工作中的各種實戰技能 (Vue 3)系列 第 22

[Day22] Vue 3 單元測試 (Unit Testing) - Testing Vuex

今天這篇文章主要想介紹兩個重點:

  • 測試使用 Vuex 的元件
  • 測試 Vuex 本身

Testing Component with Vuex

下面是一個使用了 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 的錯誤訊息

https://ithelp.ithome.com.tw/upload/images/20211007/201134870n84k4sn05.png

這是因為我們僅在 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')
})

Initialize the Vuex State every time

正常來講,在進行單元測試時,每一次的測試應該是彼此獨立的,所以也不應該會因為測試案例的順序而造成錯誤,但因為現在元件有和 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')
  })
})

Testing Vuex in Isolation

如果你想要為你的 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 的主題了,那我們明天見!


上一篇
[Day21] Vue 3 單元測試 (Unit Testing) - Props & Computed
下一篇
[Day23]Vue3 E2E Testing: Cypress 基本介紹
系列文
前端工程師在工作中的各種實戰技能 (Vue 3)30

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-10-09 16:00:16

好看的 Vue unit test 系列文結束惹~~

Mia Yang iT邦新手 5 級 ‧ 2021-10-09 16:54:13 檢舉

TD 感謝你耐心看完!

我要留言

立即登入留言