- 1. Mock Functions คืออะไร?
- 2. ติดตั้ง Vitest
- 3. เริ่มต้น Mock ด้วย vi.fn()
- 4. การ Mock ฟังก์ชันแบบ Asynchronous
- 5. ตรวจสอบการเรียกใช้งานด้วย Assertions
- 6. ใช้ vi.spyOn() เพื่อตรวจสอบเมธอดจริง
- 7. ใช้ vi.mock() จำลองทั้ง Module
- 8. เปรียบเทียบ mockClear(), mockReset() และ mockRestore()
- 9. Workshop: ทดสอบ Checkout Service โดยไม่เรียก Payment Gateway จริง
- 10. เพิ่มเติมสำหรับ Vitest 5+: vi.when() เพื่อ Mock ตามอาร์กิวเมนต์
- 11. ข้อผิดพลาดที่พบบ่อยและแนวทางแก้ไข
- 12. สรุปแบบ Cheat Sheet
- 13. แหล่งอ้างอิง

กลุ่มผู้อ่าน: ผู้พัฒนา JavaScript/TypeScript, ผู้ทดสอบซอฟต์แวร์ และผู้เรียน Unit Testing
หมายเหตุเวอร์ชัน: ตัวอย่างหลักใช้ API พื้นฐานของ Vitest; หัวข้อvi.when()ต้องใช้ Vitest 5.0.0 ขึ้นไป
#1. Mock Functions คืออะไร?
Mock Function คือฟังก์ชันที่ใช้แทนฟังก์ชันจริงระหว่างทดสอบ เพื่อควบคุมผลลัพธ์และตรวจสอบการเรียกใช้งานโดยไม่จำเป็นต้องเรียกบริการจริง เช่น Payment Gateway, ฐานข้อมูล, Email API หรือ HTTP API
Mock Function ตอบคำถามสำคัญได้ 3 ด้าน:
- Control: เมื่อถูกเรียกแล้วควรคืนค่า/โยนข้อผิดพลาดอะไร?
- Observe: ฟังก์ชันถูกเรียกกี่ครั้ง และได้รับอาร์กิวเมนต์อะไร?
- Isolate: สามารถทดสอบ Business Logic โดยไม่พึ่ง External Service ได้หรือไม่?
flowchart LR
T[Unit Test] --> S[System Under Test]
S --> D{Dependency}
D -->|Production| R[Real API / Database]
D -->|Testing| M[Mock Function]
M --> A[Controlled Result]
M --> O[Call History & Assertions]
#Mock กับ Spy ต่างกันอย่างไร?
| เครื่องมือ | ลักษณะ | พฤติกรรมเริ่มต้น | ใช้เมื่อ |
|---|---|---|---|
vi.fn() |
สร้าง Mock Function ใหม่ | คืน undefined หากไม่ได้กำหนด implementation |
ฉีด Dependency, Callback, สร้าง Stub |
vi.spyOn(object, 'method') |
ครอบเมธอดที่มีอยู่แล้ว | เรียกเมธอดจริง พร้อมบันทึกการเรียก | อยากตรวจสอบการเรียกเมธอดโดยยังคงพฤติกรรมจริง |
vi.mock(import('./module.js'), factory) |
แทนที่ Export ของโมดูล | ตามที่กำหนดใน factory หรือการ automock | ตัดการเชื่อมต่อกับ API, DB หรือโมดูลภายนอก |
vi.fn()และvi.spyOn()ใช้ API สำหรับตรวจสอบการเรียกและปรับพฤติกรรมหลายรายการร่วมกันได้ เช่นmockReturnValue,mockImplementation,.mock.calls
#2. ติดตั้ง Vitest
ตรวจสอบ Node.js ก่อน โดยเอกสาร Getting Started ของ Vitest เวอร์ชันปัจจุบันระบุ Node.js 22.12.0 ขึ้นไป และ Vite 6.4.0 ขึ้นไป สำหรับสภาพแวดล้อมที่เกี่ยวข้อง
node --version
npm --version
mkdir vitest-mock-workshop
cd vitest-mock-workshop
npm init -y
npm install -D vitest
เพิ่ม Scripts ลงใน package.json:
{
"type": "module",
"scripts": {
"test": "vitest",
"test:run": "vitest run"
}
}
ตัวอย่างนี้แสดงเฉพาะส่วนของ
package.jsonที่เกี่ยวข้อง ไม่จำเป็นต้องลบฟิลด์อื่นที่npm initสร้างไว้
สร้างไฟล์ example.test.js:
import { expect, test, vi } from 'vitest'
test('should call callback', () => {
const callback = vi.fn()
callback('hello')
expect(callback).toHaveBeenCalledOnce()
expect(callback).toHaveBeenCalledWith('hello')
})
สั่งทดสอบ:
npm run test:run
#3. เริ่มต้น Mock ด้วย vi.fn()
import { expect, test, vi } from 'vitest'
test('create a mock function', () => {
const sendEmail = vi.fn()
const result = sendEmail('student@example.com')
expect(result).toBeUndefined()
expect(sendEmail).toHaveBeenCalledTimes(1)
expect(sendEmail).toHaveBeenCalledWith('student@example.com')
})
จุดสำคัญ: vi.fn() เริ่มต้นโดย ไม่ทำงานจริง และบันทึกประวัติการเรียกใช้งานอัตโนมัติ
#3.1 กำหนดผลลัพธ์ด้วย mockReturnValue()
const getStock = vi.fn().mockReturnValue(10)
expect(getStock('book')).toBe(10)
expect(getStock('pen')).toBe(10)
ผลลัพธ์ไม่เปลี่ยนตามอาร์กิวเมนต์ เนื่องจากใช้ค่า Return แบบคงที่
#3.2 กำหนดผลลัพธ์ครั้งถัดไปด้วย mockReturnValueOnce()
const getStatus = vi.fn()
.mockReturnValue('READY')
.mockReturnValueOnce('PENDING')
.mockReturnValueOnce('PROCESSING')
expect(getStatus()).toBe('PENDING')
expect(getStatus()).toBe('PROCESSING')
expect(getStatus()).toBe('READY')
เหมาะสำหรับจำลอง State Transition หรือ Retry Logic ตามลำดับการเรียก
#3.3 สร้าง Logic จำลองด้วย mockImplementation()
const calculateDiscount = vi.fn()
.mockImplementation((price, percent) => price * (percent / 100))
expect(calculateDiscount(1000, 10)).toBe(100)
expect(calculateDiscount).toHaveBeenCalledWith(1000, 10)
สามารถเขียนสั้นด้วย vi.fn((price, percent) => ...) ได้เช่นกัน
#3.4 ใช้ mockImplementationOnce()
const search = vi.fn(() => 'NORMAL')
.mockImplementationOnce(() => 'SPECIAL')
expect(search()).toBe('SPECIAL')
expect(search()).toBe('NORMAL')
#4. การ Mock ฟังก์ชันแบบ Asynchronous
สำหรับฟังก์ชันที่คืน Promise ให้ใช้ mockResolvedValue() และ mockRejectedValue() แทนการคืนค่าด้วย mockReturnValue() เมื่ออยากสื่อความหมายของผลลัพธ์แบบ Async ให้ชัดเจน
import { expect, test, vi } from 'vitest'
test('simulate successful API response', async () => {
const fetchUser = vi.fn().mockResolvedValue({
id: 101,
name: 'Mali',
})
await expect(fetchUser(101)).resolves.toEqual({
id: 101,
name: 'Mali',
})
})
test('simulate API failure', async () => {
const fetchUser = vi.fn()
.mockRejectedValue(new Error('Service unavailable'))
await expect(fetchUser(101)).rejects.toThrow('Service unavailable')
})
ตัวอย่าง Retry Scenario:
const request = vi.fn()
.mockResolvedValue({ ok: true })
.mockRejectedValueOnce(new Error('Temporary failure'))
await expect(request()).rejects.toThrow('Temporary failure')
await expect(request()).resolves.toEqual({ ok: true })
expect(request).toHaveBeenCalledTimes(2)
ฟังก์ชันสำคัญ:
| API | ความหมาย |
|---|---|
mockResolvedValue(data) |
คืน Promise ที่สำเร็จด้วยค่า data |
mockRejectedValue(error) |
คืน Promise ที่ล้มเหลวด้วย error |
mockResolvedValueOnce(data) |
สำเร็จเฉพาะการเรียกครั้งถัดไป |
mockRejectedValueOnce(error) |
ล้มเหลวเฉพาะการเรียกครั้งถัดไป |
#5. ตรวจสอบการเรียกใช้งานด้วย Assertions
import { expect, test, vi } from 'vitest'
test('inspect call history', () => {
const logger = vi.fn((message) => `logged:${message}`)
logger('started')
logger('finished')
expect(logger).toHaveBeenCalled()
expect(logger).toHaveBeenCalledTimes(2)
expect(logger).toHaveBeenNthCalledWith(1, 'started')
expect(logger).toHaveBeenLastCalledWith('finished')
expect(logger).toHaveReturnedWith('logged:started')
expect(logger.mock.calls).toEqual([
['started'],
['finished'],
])
expect(logger.mock.results[0]).toEqual({
type: 'return',
value: 'logged:started',
})
})
| Assertion / Property | ตรวจสอบ |
|---|---|
toHaveBeenCalled() |
มีการเรียกอย่างน้อยหนึ่งครั้ง |
toHaveBeenCalledOnce() |
ถูกเรียกหนึ่งครั้ง |
toHaveBeenCalledTimes(n) |
ถูกเรียกทั้งหมด n ครั้ง |
toHaveBeenCalledWith(...) |
เคยเรียกด้วยอาร์กิวเมนต์ที่ระบุ |
toHaveBeenNthCalledWith(n, ...) |
การเรียกครั้งที่ n |
toHaveBeenLastCalledWith(...) |
การเรียกครั้งล่าสุด |
toHaveReturnedWith(value) |
เคยคืนค่าที่ระบุแบบ synchronous |
.mock.calls |
อาร์เรย์รายการอาร์กิวเมนต์ของทุกการเรียก |
.mock.results |
สถานะและค่าที่คืนหรือข้อผิดพลาดที่โยน |
ระวัง:
.mock.callsเก็บ reference ของออบเจ็กต์ที่ส่งเข้าไป ไม่ได้ Snapshot ไว้ หากออบเจ็กต์ถูกแก้ไขภายหลัง ประวัติการเรียกจะสะท้อนสถานะใหม่ หากต้องตรวจสอบข้อมูล ณ เวลาที่เรียก ให้ clone ค่าใน mock implementation หรือ Assert ก่อนแก้ไขออบเจ็กต์
#6. ใช้ vi.spyOn() เพื่อตรวจสอบเมธอดจริง
import { expect, test, vi } from 'vitest'
test('spy on existing method', () => {
const calculator = {
add(a, b) {
return a + b
},
}
const spy = vi.spyOn(calculator, 'add')
expect(calculator.add(4, 5)).toBe(9) // ทำงานจริงตามเดิม
expect(spy).toHaveBeenCalledWith(4, 5)
spy.mockReturnValue(100) // เปลี่ยนพฤติกรรม
expect(calculator.add(4, 5)).toBe(100)
spy.mockRestore() // คืนเมธอดต้นฉบับ
expect(calculator.add(4, 5)).toBe(9)
})
ข้อควรจำ: vi.spyOn() เริ่มติดตามการเรียกได้หลังสร้าง Spy เท่านั้น การเรียกก่อนหน้าไม่ถูกบันทึก ใน Vitest Browser Mode ไม่สามารถใช้ Spy กับ Export ของ Native ESM Module โดยตรงได้ทุกกรณี; ใช้ vi.mock('./module.js', { spy: true }) แทนสำหรับกรณีดังกล่าว
#7. ใช้ vi.mock() จำลองทั้ง Module
สมมติว่ามีโมดูล src/user-api.js:
export async function fetchUser(id) {
// ในระบบจริงอาจใช้ fetch() หรือฐานข้อมูล
throw new Error('External API should not be called in unit tests')
}
และ Service ที่ใช้โมดูลดังกล่าว src/user-greeter.js:
import { fetchUser } from './user-api.js'
export async function greeting(userId) {
const user = await fetchUser(userId)
return `สวัสดี ${user.name}`
}
สร้างไฟล์ tests/user-greeter.test.js:
import { expect, test, vi } from 'vitest'
import { fetchUser } from '../src/user-api.js'
import { greeting } from '../src/user-greeter.js'
vi.mock(import('../src/user-api.js'), () => ({
fetchUser: vi.fn(),
}))
test('greeting uses mocked API data', async () => {
vi.mocked(fetchUser).mockResolvedValue({ id: 7, name: 'มะลิ' })
await expect(greeting(7)).resolves.toBe('สวัสดี มะลิ')
expect(fetchUser).toHaveBeenCalledWith(7)
})
ทำไมต้อง vi.mocked(...)? เป็น TypeScript-friendly helper สำหรับเข้าถึงชนิดข้อมูลของ Mock Function อย่างถูกต้อง; ไม่ได้สร้าง Mock ขึ้นมาเอง จึงต้องใช้คู่กับฟังก์ชันที่ถูก Mock อยู่แล้ว
Hoisting: vi.mock() ถูกย้ายขึ้นไปรันก่อนการ Import ในกระบวนการแปลงไฟล์ของ Vitest จึงห้ามอ้างตัวแปรที่ประกาศไว้ภายนอก Factory แบบทั่วไป หากจำเป็นให้ใช้ vi.hoisted() หรือ vi.doMock() ตามบริบท แนะนำให้ส่ง import('./module.js') เพื่อให้ TypeScript ตรวจสอบชนิดข้อมูลของ Factory
ตัวอย่างการ Mock บาง Export แต่คงส่วนอื่นไว้:
vi.mock(import('../src/math.js'), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
expensiveCalculation: vi.fn(() => 42),
}
})
หมายเหตุ: ตัวอย่าง Partial Mock นี้เป็น Pattern สำหรับไฟล์
math.jsที่มีอยู่จริง ไม่ได้รวมอยู่ในโครงงาน Workshop ที่ให้ดาวน์โหลด
#8. เปรียบเทียบ mockClear(), mockReset() และ mockRestore()
| คำสั่ง | ล้างประวัติการเรียก | Reset implementation | ยกเลิก Spy และคืนเมธอดต้นฉบับ |
|---|---|---|---|
mockClear() |
✅ | ❌ | ❌ |
mockReset() |
✅ | ✅ | ❌ |
mockRestore() |
✅ | ✅ | ✅ เฉพาะ Spy ที่สร้างด้วย vi.spyOn() |
รายละเอียดสำคัญ:
vi.fn()ที่สร้างโดยไม่มี implementation เมื่อmockReset()จะกลับมาคืนundefinedส่วนvi.fn(originalImpl)เมื่อ Reset จะกลับไปใช้ originalImpl ที่ใส่ไว้ตอนสร้าง ไม่ใช่คืนundefinedเสมอไป
import { expect, test, vi } from 'vitest'
test('reset to initial implementation', () => {
const double = vi.fn((n) => n * 2)
double.mockReturnValue(99)
expect(double(3)).toBe(99)
double.mockClear()
expect(double(3)).toBe(99) // implementation ยังอยู่
double.mockReset()
expect(double(3)).toBe(6) // กลับเป็น implementation เริ่มต้น
})
แบบทั้ง Test File:
import { afterEach, vi } from 'vitest'
afterEach(() => {
vi.clearAllMocks() // ล้างประวัติการเรียก
vi.restoreAllMocks() // คืนเมธอดที่เคย spyOn
})
ค่าตั้งต้นใน Vitest รุ่นปัจจุบัน clearMocks คือ true, restoreMocks คือ false และ mockReset คือ false ควรตรวจสอบ Config ที่ใช้จริงเสมอ โดยเฉพาะกรณีรัน Test แบบ Concurrent
// vitest.config.js
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
clearMocks: true,
restoreMocks: true,
},
})
อย่าสับสน: vi.restoreAllMocks() คืนเมธอดของ Spy ที่สร้างด้วย vi.spyOn() เป็นหลัก และ ไม่ใช่ การ Reset ทุก vi.fn() หรือทุก Module Mock นอกจากนี้ไม่ล้างประวัติของตัว Spy ตามลักษณะเดียวกับการเรียก spy.mockRestore() โดยตรง
#9. Workshop: ทดสอบ Checkout Service โดยไม่เรียก Payment Gateway จริง
#9.1 สถาปัตยกรรมตัวอย่าง
flowchart TB
T[checkout.test.js] -->|เรียก checkout| S[Checkout Service]
S -->|charge| P[Payment Gateway Dependency]
S -->|notify| N[Notification Dependency]
T -->|vi.fn: success / decline / reject| P
T -->|vi.fn + assert calls| N
#9.2 ไฟล์ src/checkout.js
export async function checkout({ charge, notify }, order) {
if (!order?.id || !Number.isFinite(order.total) || order.total <= 0) {
throw new Error('Invalid order')
}
const payment = await charge({
orderId: order.id,
amount: order.total,
})
if (!payment.ok) {
return { status: 'declined', orderId: order.id }
}
await notify(order.id)
return {
status: 'paid',
transactionId: payment.transactionId,
}
}
Service นี้ใช้หลักการ Dependency Injection โดยรับ charge() และ notify() จากภายนอก ทำให้ทดสอบโดยไม่ต้องใช้ Gateway หรือบริการแจ้งเตือนจริง
#9.3 ไฟล์ tests/checkout.test.js
import { describe, expect, test, vi } from 'vitest'
import { checkout } from '../src/checkout.js'
describe('checkout', () => {
const order = { id: 'ORD-001', total: 1250 }
test('successful payment sends notification', async () => {
const charge = vi.fn().mockResolvedValue({
ok: true,
transactionId: 'TXN-900',
})
const notify = vi.fn().mockResolvedValue(undefined)
await expect(checkout({ charge, notify }, order)).resolves.toEqual({
status: 'paid',
transactionId: 'TXN-900',
})
expect(charge).toHaveBeenCalledExactlyOnceWith({
orderId: 'ORD-001', amount: 1250,
})
expect(notify).toHaveBeenCalledWith('ORD-001')
})
test('declined payment does not send notification', async () => {
const charge = vi.fn().mockResolvedValue({ ok: false })
const notify = vi.fn()
await expect(checkout({ charge, notify }, order)).resolves.toEqual({
status: 'declined',
orderId: 'ORD-001',
})
expect(notify).not.toHaveBeenCalled()
})
test('gateway failure propagates without notification', async () => {
const charge = vi.fn().mockRejectedValue(new Error('Gateway timeout'))
const notify = vi.fn()
await expect(checkout({ charge, notify }, order))
.rejects.toThrow('Gateway timeout')
expect(notify).not.toHaveBeenCalled()
})
test('invalid input does not charge customer', async () => {
const charge = vi.fn()
const notify = vi.fn()
await expect(checkout({ charge, notify }, { id: 'X', total: 0 }))
.rejects.toThrow('Invalid order')
expect(charge).not.toHaveBeenCalled()
})
})
สิ่งที่ควรตรวจสอบใน Workshop: Happy Path, Declined Payment, API Error และ Invalid Input พร้อมตรวจสอบว่าไม่มี Side Effect เกิดขึ้นโดยไม่จำเป็น
#9.4 รันทดสอบ
npm run test:run
# หรือ
npx vitest run
ตัวอย่างชุดไฟล์ในไฟล์ ZIP ที่จัดเตรียมให้:
vitest-mock-workshop/
├── package.json
├── vitest.config.js
├── src/
│ ├── checkout.js
│ ├── user-api.js
│ └── user-greeter.js
└── tests/
├── basic-mocks.test.js
├── checkout.test.js
└── user-greeter.test.js
#10. เพิ่มเติมสำหรับ Vitest 5+: vi.when() เพื่อ Mock ตามอาร์กิวเมนต์
ตั้งแต่ Vitest 5.0.0 มี vi.when() สำหรับกำหนดพฤติกรรมแยกตาม Argument โดยไม่ต้องเขียน if/else ใน mockImplementation() เอง
import { expect, test, vi } from 'vitest'
test('mock different user IDs', async () => {
const findById = vi.fn()
vi.when(findById)
.calledWith(1)
.thenResolve({ id: 1, name: 'Alice' })
.calledWith(2)
.thenResolve({ id: 2, name: 'Bob' })
await expect(findById(1)).resolves.toEqual({ id: 1, name: 'Alice' })
await expect(findById(2)).resolves.toEqual({ id: 2, name: 'Bob' })
})
เมื่อใช้
vi.when()ต้องระวังลำดับของcalledWith()โดย Matcher แบบเฉพาะเจาะจงควรอยู่ก่อน Matcher แบบกว้าง และดูรายละเอียดการ Cleanup ในเอกสาร API
#11. ข้อผิดพลาดที่พบบ่อยและแนวทางแก้ไข
- สร้าง Spy หลังจากฟังก์ชันถูกเรียกไปแล้ว: Spy จะไม่เห็นการเรียกย้อนหลัง จึงควรติดตั้ง Spy ก่อน Trigger ระบบที่ต้องการทดสอบ
- ลืม Reset หรือ Restore Mock: ค่า Implementation และประวัติการเรียกอาจรั่วข้าม Test Cases ให้ใช้ Lifecycle Hooks หรือ Config อย่างเหมาะสม
- คิดว่า
vi.mocked()สร้าง Mock: จริง ๆ เป็น Type Helper สำหรับค่าที่เป็น Mock อยู่แล้ว - อ้างตัวแปรภายนอกใน
vi.mock()Factory: อาจเกิดปัญหา Hoisting ให้ใช้vi.hoisted()หรือปรับโครงสร้างโค้ด - Mock ทุกอย่างจน Test ไม่มีความหมาย: ควร Mock เฉพาะ Boundary ของระบบ เช่น HTTP, DB, เวลา หรือบริการภายนอก และทดสอบ Business Logic จริง
- อาศัย
.mock.callsเพื่อตรวจสอบ Snapshot ของ Object ที่มีการเปลี่ยนค่า: ต้อง Clone ณ เวลาเรียกหรือ Assert ก่อนเกิด Mutation - ใช้
vi.spyOn()กับ ESM Exports ใน Browser Mode: ใช้ Module Mock แบบ{ spy: true }แทนตามข้อจำกัดของ Runtime - เข้าใจผิดว่า
vi.resetAllMocks()=vi.restoreAllMocks(): อันแรก Reset Implementation และประวัติ; อันหลังคืนเมธอดของ Spy ให้ Object ดั้งเดิม - ทดสอบ Asynchronous Code แต่ลืม
await: ผลการทดสอบอาจผ่านผิด ๆ ทั้งที่ Assertion ของ Promise ยังไม่เสร็จ - พยายาม Mock ฟังก์ชันที่เรียกกันภายในไฟล์เดียวกันผ่าน Module Boundary: Vitest อาจไม่สามารถแทรกแซง Internal Reference ได้ ควรแยกโมดูลหรือใช้ Dependency Injection
#12. สรุปแบบ Cheat Sheet
| ต้องการทำอะไร | คำสั่งที่ควรใช้ |
|---|---|
| สร้าง Mock Function | vi.fn() |
| ติดตามเมธอดเดิม | vi.spyOn(obj, 'method') |
| แทนโมดูลภายนอก | vi.mock(import('./api.js'), () => ({ ... })) |
| คืนค่าคงที่ | .mockReturnValue(value) |
| คืนค่าเพียงหนึ่งครั้ง | .mockReturnValueOnce(value) |
| จำลอง Logic | .mockImplementation(fn) |
| Mock Async Success | .mockResolvedValue(value) |
| Mock Async Error | .mockRejectedValue(error) |
| ตรวจสอบจำนวนครั้ง | expect(fn).toHaveBeenCalledTimes(n) |
| ตรวจสอบอาร์กิวเมนต์ | expect(fn).toHaveBeenCalledWith(...) |
| อ่านประวัติการเรียก | fn.mock.calls |
| ล้างประวัติ | .mockClear() หรือ vi.clearAllMocks() |
| Reset การทำงาน | .mockReset() หรือ vi.resetAllMocks() |
| คืนเมธอดต้นฉบับ | .mockRestore() หรือ vi.restoreAllMocks() สำหรับ Spies |
| คืนค่าตามอาร์กิวเมนต์ (Vitest 5+) | vi.when(mock).calledWith(...).thenReturn(...) |
#13. แหล่งอ้างอิง
- Vitest — Mock Functions (Learn): https://vitest.dev/guide/learn/mock-functions
- Vitest — Mocking Functions: https://vitest.dev/guide/mocking/functions
- Vitest — Mock API: https://vitest.dev/api/mock.html
- Vitest —
viUtility: https://vitest.dev/api/vi - Vitest — Mocking Modules: https://vitest.dev/guide/mocking/modules
- Vitest — Conditional Mocking with
vi.when: https://vitest.dev/guide/recipes/conditional-mocking - Vitest — Getting Started: https://vitest.dev/guide/
- Vitest —
clearMocksConfig: https://vitest.dev/config/clearmocks
บทสรุป: ใช้ vi.fn() เมื่อต้องสร้าง Dependency จำลอง, ใช้ vi.spyOn() เมื่อต้องการสังเกตเมธอดที่มีอยู่แล้ว และใช้ vi.mock() เมื่อจำเป็นต้องแทนโมดูลภายนอกทั้งหมดหรือบางส่วน โดยให้ความสำคัญกับ Assertions และ Mock Cleanup เพื่อให้ Unit Tests มีความน่าเชื่อถือ