各位寫 Vue 3 的朋友,有沒有過這種經驗:改了一個小功能,結果另一個頁面莫名其妙壞掉?或者重構時手在發抖,深怕動了哪裡就炸?這時候「單元測試」就是你的安全網。今天我們用 Vitest(Vite 官方推薦的測試框架)搭配 @vue/test-utils,從零把 Vue 3 專案的測試跑起來。
一、為什麼選 Vitest
- 啟動極快:與 Vite 共用設定與轉譯管線,不必另起一個 babel/jest 環境。
- 原生支援:ESM、TypeScript、JSX 通通開箱即用,不用額外配置。
- API 相容 Jest:describe / it / expect 幾乎一致,從 Jest 遷移成本極低。
重點:若你本來就用 Vite 開發,Vitest 幾乎是零摩擦的選擇,不需要為了測試再養一套工具鏈。
二、安裝與設定
步驟 1:安裝開發依賴。
npm i -D vitest @vue/test-utils jsdom
步驟 2:在 vite.config.ts 加上 test 設定區塊。
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
globals: true,
},
})
步驟 3:在 package.json 加上腳本,方便日常執行。
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
三、寫第一個元件測試
以一個計數器元件 Counter.vue 為例。
<template>
<button @click="count++">{{ count }}</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
對應的測試檔 Counter.test.ts 如下。
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('點擊按鈕後數字加 1', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
})
重點:mount 會把元件渲染成可互動的 DOM,trigger 模擬使用者事件;非同步的操作要記得 await。
四、測試 Pinia store
實戰中我們常用 Pinia 管理狀態,測試時要用 createPinia 注入一個乾淨的環境。
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from './cart'
beforeEach(() => setActivePinia(createPinia()))
it('加入商品後總額更新', () => {
const cart = useCartStore()
cart.add('apple', 30)
expect(cart.total).toBe(30)
})
每個測試前重置 Pinia,才能保證用例之間互不污染。
五、常見陷阱與最佳實踐
- 測行為而非實作:關注使用者看到的結果(文字、事件),不要去測試內部變數名稱。
- 狀態要隔離:beforeEach 裡重置 Pinia / 元件狀態,避免上一個用例影響下一個。
- 非同步要等:trigger、nextTick、await 的 Promise 都要確實等待完成。
特別注意:globals: true 才不用每個檔案手動 import describe/it/expect;若設為 false,記得每個測試檔都要自行 import。
互動話題:你在專案裡有寫測試的習慣嗎?最常被測試卡住的環節是什麼?歡迎留言分享,我們一起把「不敢改」變成「隨便改」。