哈嘍,各位小夥伴,歡迎來到我是wangfang呀的博客!我是我是wangfang呀,雖然還在編程的“菜鳥”階段,但我已經迫不及待地想和大家分享我一路上踩過的坑和學到的小技巧。如果你也曾為bug頭疼,那麼你來對地方了!今天的內容希望能夠給大家帶來一些靈感和幫助。

前言

一句話先立旗: 寫測試 ≠ 浪費時間,寫測試 = 把 BUG 容易出現的地方提前炸出來。 這一篇圍繞 四大關鍵詞 —— 測試框架選型、vue-test-utils、組件單測套路、Mock/Spy/快照/覆蓋率 — 帶你把 Vue 3 項目測試跑通、跑快、跑穩。


1. 測試框架選型:Jest vs Mocha vs Vitest

維度 Jest Mocha + Chai Vitest
維護方 Meta 社區 Vite 核心團隊
斷言 / Mock 內置 需 Chai / Sinon 內置,Jest API 對齊
運行速度 基於 JSDOM,Cold start 較慢 快,配置靈活 極快(esbuild + 原生 ESM),HMR 體驗
TS 支持 ts-jest / babel-jest ts-node/register 內置 esbuild 編譯
Vue 官方 Demo 早期 CLI 默認 早期選項 Vite 模板默認
適用場景 老項目 / 大型生態依賴 極度自定義 Vue3 + Vite 項目首選

結論

  • 全新 Vite + Vue3:直接上 Vitest(幾乎 100% 兼容 Jest API)
  • 舊 CLI(webpack)或依賴 Jest 插件生態:繼續 Jest
  • 需要極高級自定義、非 Node 環境:Mocha 自由度最高

2. @vue/test-utils 基本用法

2.1 安裝

pnpm add -D @vue/test-utils vitest      # 示例以 Vitest 為主

2.2 核心 API 速查表

API 作用
mount() 完整渲染組件(包含子組件)
shallowMount() 淺渲染 —— 子組件自動 stub
wrapper.find(selector) 查詢元素 / 子組件
wrapper.get() find 同,但未找到會拋錯
wrapper.setProps() 更新 props(異步)
wrapper.setValue() 更改 <input> 值並觸發 input 事件
wrapper.emitted() 獲取組件 $emit 記錄
flushPromises() 等待異步(Promise 微任務)
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'

test('按鈕點擊後計數 +1', async () => {
  const wrapper = mount(Counter)
  expect(wrapper.text()).toContain('0')

  await wrapper.find('button').trigger('click')
  expect(wrapper.text()).toContain('1')
})

3. 組件單元測試套路

3.1 場景拆解

測什麼 關注點
渲染 DOM 是否按 props / slots 呈現
交互 點擊、輸入後 state 變化、事件觸發
業務邏輯 計算屬性、watcher、方法輸出
副作用 發請求、存儲、路由跳轉等(需 Mock)

3.2 示例組件 TodoItem.vue

<template>
  <li :class="{ done: item.done }">
    <input type="checkbox" v-model="checked" />
    <span>{{ item.text }}</span>
  </li>
</template>

<script setup>
const props = defineProps<{ item: { id: number; text: string; done: boolean } }>()
const emit = defineEmits(['toggle'])
const checked = useVModel(props, 'item.done')   // 假設封裝

watch(checked, val => emit('toggle', props.item.id, val))
</script>

3.3 單測示例

import { mount } from '@vue/test-utils'
import TodoItem from '@/components/TodoItem.vue'

const factory = (overrides = {}) =>
  mount(TodoItem, {
    props: {
      item: { id: 1, text: 'Play', done: false },
      ...overrides
    }
  })

test('初始渲染', () => {
  const w = factory()
  expect(w.text()).toContain('Play')
  expect(w.classes()).not.toContain('done')
})

test('勾選後觸發 toggle 事件', async () => {
  const w = factory()
  await w.find('input').setValue(true)

  expect(w.emitted('toggle')![0]).toEqual([1, true])
  expect(w.classes()).toContain('done')
})

4. Mock / Spy / 快照 / 覆蓋率

4.1 Mock 函數 & 模塊

import { vi } from 'vitest'
vi.mock('@/api', () => ({
  fetchUser: vi.fn(() => Promise.resolve({ name: 'Alice' }))
}))
  • 與 Jest 的 jest.mock 等價
  • 可在測試用例內部動態修改返回值

4.2 Spy 監聽

import { fetchUser } from '@/api'

test('調用次數', async () => {
  const spy = vi.spyOn(api, 'fetchUser')
  await getUserProfile()
  expect(spy).toHaveBeenCalledTimes(1)
})

4.3 快照測試

const wrapper = mount(Navbar)
expect(wrapper.html()).toMatchSnapshot()
  • 首次執行會生成 .snap 文件
  • 結構大改需 --update 更新快照
  • 謹慎使用:佈局頻繁改動的組件會導致快照紅炸

4.4 覆蓋率

vitest run --coverage
# 或 jest --coverage

生成 coverage/ 報告(html / text)。

  • 關注 行覆蓋(Line) + 分支覆蓋(Branch)
  • 不必追求 100%,但核心業務 / 邊界分支最好全覆蓋

⚠️ 實戰踩坑 & 性能提示

問題 解決
異步更新斷言失敗 await nextTick()flushPromises() 再斷言
Vitest 與 JSDOM API 不兼容 testEnvironment: 'jsdom' 已內置;若需元素尺寸,用 vi.stubGlobal('getComputedStyle', …)
Vue Router/Pinia 注入 global.plugins: [router, pinia]wrapper = mount(App, { global: { plugins: […] } })
大倉庫測試慢 按需 --run 指定 pattern、開啓 threads:false 調單核調試
快照忽略動態 ID expect(html).toMatchSnapshot({ ignore: [/id="[^"]*/] })

🚀 結語

  • 框架選型:新   ◀︎ Vitest;老   ◀︎ Jest
  • 測試金字塔:單元(多) → 組件集成 → E2E(少)
  • vue-test-utilsmount / find / trigger / emitted 四件套最常用
  • Mock & Spy:隔絕網絡和副作用,讓測試純粹、可復現
  • 覆蓋率:用來發現盲區,不是 KPI 的數字比賽

“測試寫得好,需求改得早”。—— 當需求來回變而你依舊底氣十足,就是測試真正的價值所在!

好啦,今天的內容就先到這裏!如果覺得我的分享對你有幫助,給我點個贊,順便評論吐個槽,當然不要忘了三連哦!感謝大家的支持,記得常回來,我是wangfang呀等着你們的下一次訪問!