跳至主要内容

08|Vitest 進階技巧與最佳實踐

系列文章:從零學會 Vitest — 第 8 篇,共 8 篇(最終章)


Snapshot 測試

Snapshot(快照)測試會把元件或資料的輸出結果「拍一張照」存起來,之後每次跑測試都比對有沒有改變。

適合用在:

  • UI 元件的 HTML 結構
  • 序列化輸出(JSON、文字格式)
  • 不常變動但想確保沒有意外改變的東西

基本用法

import { render } from '@testing-library/react'
import { Greeting } from './Greeting'

it('Greeting 的渲染結果應該維持一致', () => {
const { container } = render(<Greeting name="Alice" />)
expect(container).toMatchSnapshot()
})

第一次跑時,Vitest 會在 __snapshots__/ 資料夾建立一個 .snap 檔案:

// Greeting.test.tsx.snap
exports[`Greeting 的渲染結果應該維持一致 1`] = `
<div>
<h1>
哈囉,Alice!
</h1>
</div>
`;

之後跑測試都會拿實際輸出和這個快照比對。如果元件改了,快照也要更新:

# 更新所有快照
npx vitest --update-snapshots
# 或縮寫
npx vitest -u

行內 Snapshot

不想產生外部檔案,可以把快照直接寫在測試裡:

it('格式化結果', () => {
const result = formatDate(new Date('2024-01-15'))
expect(result).toMatchInlineSnapshot(`"2024年1月15日"`)
})

參數化測試:test.each

當你要用很多不同的輸入測試同一個函式,用 test.each 避免重複:

// ❌ 重複的寫法
it('0 的二倍是 0', () => expect(double(0)).toBe(0))
it('1 的二倍是 2', () => expect(double(1)).toBe(2))
it('5 的二倍是 10', () => expect(double(5)).toBe(10))

// ✅ 用 test.each
test.each([
[0, 0],
[1, 2],
[5, 10],
[-3, -6],
])('double(%i) 應該是 %i', (input, expected) => {
expect(double(input)).toBe(expected)
})

也可以用物件陣列,讓測試名稱更易讀:

test.each([
{ price: 100, discount: 0.2, expected: 80 },
{ price: 200, discount: 0.5, expected: 100 },
{ price: 50, discount: 0, expected: 50 },
])('$price 元打折後應該是 $expected 元', ({ price, discount, expected }) => {
expect(applyDiscount(price, discount)).toBe(expected)
})

共用測試設定:setupFiles

如果每個測試檔都需要相同的初始設定(例如 Mock 某個全域物件),可以集中放在 setupFiles

// vite.config.ts
export default defineConfig({
test: {
globals: true,
setupFiles: ['./src/test-setup.ts'],
},
})
// src/test-setup.ts
import '@testing-library/jest-dom'

// Mock 全域的 localStorage
Object.defineProperty(window, 'localStorage', {
value: {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
},
})

// 每個測試後清除所有 Mock
afterEach(() => {
vi.clearAllMocks()
})

測試隔離:避免測試互相影響

測試之間應該完全獨立,一個測試的狀態不能影響另一個。

常見問題:共享可變狀態

// ❌ 錯誤:items 在測試之間被共享
const items = []

it('加入一個元素', () => {
items.push('A')
expect(items).toHaveLength(1)
})

it('應該是空的', () => {
// 這個測試會失敗,因為上一個測試污染了 items
expect(items).toHaveLength(0)
})
// ✅ 正確:在 beforeEach 初始化
let items: string[]

beforeEach(() => {
items = []
})

it('加入一個元素', () => {
items.push('A')
expect(items).toHaveLength(1)
})

it('應該是空的', () => {
expect(items).toHaveLength(0) // ✅ 通過
})

模組 Mock 的幾個技巧

只 Mock 模組的部分方法

vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>()
return {
...actual, // 保留其他方法
formatDate: vi.fn().mockReturnValue('假日期'), // 只替換這個
}
})

Mock 環境變數

it('在 production 環境應該隱藏 debug 資訊', () => {
const originalEnv = process.env.NODE_ENV
process.env.NODE_ENV = 'production'

// ... 測試邏輯

process.env.NODE_ENV = originalEnv // 記得還原
})

命名與組織最佳實踐

測試名稱:描述行為,不描述實作

// ❌ 描述實作細節
it('呼叫 setState 並傳入新的 count', () => {})

// ✅ 描述使用者看到的行為
it('點擊 +1 後,畫面上的數字應該增加', () => {})

用 AAA 模式組織測試內容

每個測試分成三個區塊:

it('折扣計算正確', () => {
// Arrange(準備):設定測試資料
const price = 100
const discount = 0.2

// Act(執行):呼叫被測試的函式
const result = applyDiscount(price, discount)

// Assert(斷言):確認結果
expect(result).toBe(80)
})

測試檔案組織建議

src/
components/
Button/
Button.tsx
Button.test.tsx ← 放在元件旁邊
services/
api.ts
api.test.ts
utils/
math.ts
math.test.ts

效能:並行 vs 循序

Vitest 預設會並行執行不同的測試檔案,加速整體速度。

如果測試之間有相依關係(例如都操作同一個資料庫),可以設定循序執行:

// vite.config.ts
export default defineConfig({
test: {
pool: 'forks',
poolOptions: {
forks: {
singleFork: true, // 所有測試用同一個 worker 跑
},
},
},
})

或在單一測試檔案裡強制循序:

// 在檔案最頂端加上
import { describe } from 'vitest'

describe.sequential('資料庫操作', () => {
it('先新增', () => { /* ... */ })
it('再查詢', () => { /* ... */ })
it('最後刪除', () => { /* ... */ })
})

整個系列重點回顧

篇數主題最重要的一件事
01為什麼要寫測試測試讓你安心改程式
02安裝與第一個測試npm install -D vitest 就夠了
03基本語法describe + it + expect
04Mock 與 Spy隔離外部依賴,測試更穩定
05非同步測試async/await + 假計時器
06React 元件測試用語意化查詢,貼近使用者視角
07覆蓋率與 CI自動化讓品質有保障
08進階技巧Snapshot、參數化、測試隔離

繼續學習的資源


最後的話

測試是一種習慣,一開始寫起來可能覺得麻煩,但當你第一次因為測試抓到了一個你沒想到的 bug,就會開始愛上它。

從最小的單元函式開始,慢慢建立起測試習慣,你的程式碼品質和開發信心都會大幅提升。

祝測試都綠燈 ✅


本系列文章共 8 篇,適合 JavaScript / TypeScript 初學者。 — 完 —