| name | test-doubles |
| description | Test Doubles 最佳實踐指南(Vitest + TypeScript)。當需要選擇 Mock/Stub/Fake/Spy/Dummy、或討論測試隔離策略時使用。 |
Test Doubles 最佳實踐
五種類型
| 類型 | 目的 | 有行為? | 會驗證? |
|---|
| Dummy | 佔位,不會被用 | ❌ | ❌ |
| Stub | 回傳固定值 | ✅ 固定 | ❌ |
| Fake | 簡化但可運作 | ✅ 真實 | ❌ |
| Spy | 記錄呼叫 | ✅ 原行為 | ✅ 事後 |
| Mock | 預設行為 + 事先預期 | ✅ | ✅ 事先 |
優先順序(從好到壞)
Fake > Stub > Spy > Mock > Dummy
原因:
- Fake 最接近真實,測試最能反映真行為(如 in-memory DB)
- Stub 簡單隔離慢/外部依賴
- Spy 不改行為只觀察,破壞性低
- Mock 過度 mock 會導致測試通過但真實壞掉
- Dummy 不得已的佔位
Vitest 實作
Spy(記錄原物件方法呼叫)
const spy = vi.spyOn(console, 'log')
myFunction()
expect(spy).toHaveBeenCalledWith('hello')
spy.mockRestore()
Mock/Stub(vi.fn)
const onSave = vi.fn()
const fetchUser = vi.fn().mockResolvedValue({ id: 1 })
expect(onSave).toHaveBeenCalledTimes(1)
expect(onSave).toHaveBeenCalledWith({ name: 'x' })
模組替換(vi.mock)
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}))
import { fetchUser } from './api'
vi.hoisted(解決 hoisting)
vi.mock 被提升到檔頂,所以不能直接引用外部變數。要共享 mock instance:
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }))
vi.mock('./api', () => ({ fetchUser: mockFetch }))
beforeEach(() => mockFetch.mockReset())
Fake(簡化實作)
class FakeCubeStorage implements CubeStorage {
private data = new Map<string, string>()
save(id: string, state: string) { this.data.set(id, state) }
load(id: string) { return this.data.get(id) ?? null }
}
const store = new CubeService(new FakeCubeStorage())
Fake 優於 Mock 的時機:被依賴的介面多次呼叫、行為有狀態累積(DB、cache、queue)。
清理策略
import { afterEach, vi } from 'vitest'
afterEach(() => {
vi.clearAllMocks()
})
或 vite.config.ts 設 test.clearMocks: true 自動做。
TypeScript 技巧
import { fetchUser } from './api'
const mockedFetch = vi.mocked(fetchUser)
mockedFetch.mockResolvedValue({ id: 1 })
vi.mock('./api', async (importOriginal) => {
const actual = await importOriginal<typeof import('./api')>()
return { ...actual, fetchUser: vi.fn() }
})
常見陷阱
- Mock 實作細節:
expect(mock).toHaveBeenCalled() 卻沒看結果 → 測試耦合實作。應測最終行為。
- 過度 mock:mock 掉半個系統 → 測試綠燈,整合壞掉。Fake 或 real 優先。
- Mock leak:測試之間 state 殘留 → 一律
afterEach 清。
- mock 時機錯:
vi.mock 提升到檔頂,vi.doMock 則不提升(動態 mock)。
- Spy 沒 restore:後續測試 console/method 被污染。
mockRestore() 或 restoreMocks: true。
- Mock Date/Timer:
vi.useFakeTimers() / vi.setSystemTime(),記得 vi.useRealTimers() 還原。
決策流程
需要隔離嗎?
├─ 不需要 → 直接用真實依賴
└─ 需要(慢、外部、不確定性)
├─ 有狀態、呼叫多 → Fake
├─ 單次回固定值 → Stub (vi.fn().mockReturnValue)
├─ 驗證被呼叫 → Spy (vi.spyOn)
└─ 複雜預期互動 → Mock(最後選擇)