[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 的主题了,那我们明天见!


<<:  Day22 Android - RxJava(Observer+Observable)

>>:  TailwindCSS 从零开始 - 价目表卡片实战 - 进阶卡片样式

就决定是你了!嵌入式系统

本篇提到的故事是发生在我跟教授 B 签完指导教授确认单到发生意外之间。 进入正题 昨天有提到,B 教...

Day 19. UI 设计软件- Figma 简介与优势

前二篇解释了 GUI Design 阶段的重点,也提到此时花费设计师的工时相当可观。功欲善其事,必先...

Day 29 - State Monad IV

Review 由於 State 原本可以一篇写完的,被我拖成四篇的关系,所以来回顾一下,哈哈哈哈哈哈...

[Day 28] Edge Impulse + BLE Sense实现影像分类(下)

=== 书接上回,[Day 27] Edge Impulse + BLE Sense实现影像分类(上...

[Angular] Day19. Dependency providers

在上一篇中提到了如何建立与使用一个 Service,也大概介绍了什麽是 Dependency Inj...