목 함수
목 함수 (Mock Functions)
테스트를 작성하다 보면 실제 함수나 모듈을 내가 통제하는 버전으로 바꿔야 할 때가 반드시 와요. 이걸 모킹(mocking) 이라고 불러요. 이유는 여러 가지가 있어요 — 실제 함수가 테스트를 느리게 만드는 네트워크 요청을 보낸다거나, 실제 코드로는 재현하기 어려운 에러를 시뮬레이션해야 한다거나 할 때죠. 목 함수는 의존성이 무엇을 반환할지 조절하고, 어떻게 호출됐는지 관찰하며, 테스트 대상 코드를 부수 효과에서 격리시켜 줘요. Vitest 는 이 모킹 유틸들을 vi 객체로 제공해요.
목 함수 만들기
목을 만드는 가장 단순한 방법은 vi.fn() 이에요. 기본적으로 아무것도 하지 않는(그래서 undefined 를 반환하는) 함수를 주는데, 대신 자기에게 걸린 모든 호출을 추적해요:
import { expect, test, vi } from 'vitest'
test('mock function basics', () => {
const getApples = vi.fn()
// Call it
getApples()
// Check it was called
expect(getApples).toHaveBeenCalled()
expect(getApples).toHaveBeenCalledTimes(1)
// By default, a mock returns undefined
expect(getApples()).toBeUndefined()
})
목 반환값
항상 undefined 만 반환하는 목은 그 자체로는 별 쓸모가 없어요. 보통은 반환값을 조절해서 코드가 서로 다른 값에 어떻게 반응하는지 테스트하고 싶을 거예요:
import { expect, test, vi } from 'vitest'
test('mock return values', () => {
const getApples = vi.fn()
// Always return this value
getApples.mockReturnValue(10)
expect(getApples()).toBe(10)
// Return this value only once, then fall back to the default
getApples.mockReturnValueOnce(20)
expect(getApples()).toBe(20) // 20 (one-time)
expect(getApples()).toBe(10) // back to default
})
모킹하는 함수가 비동기라면 mockResolvedValue 와 mockRejectedValue 로 promise 의 결과를 조절해요:
test('mock async return values', async () => {
const fetchUser = vi.fn()
fetchUser.mockResolvedValue({ name: 'Alice' })
const user = await fetchUser()
expect(user.name).toBe('Alice')
fetchUser.mockRejectedValue(new Error('Not found'))
await expect(fetchUser()).rejects.toThrow('Not found')
})
::: tip
mockReturnValue 는 목이 받는 인자와 무관하게 항상 같은 값을 반환해요. 인자에 따라 반환값을 다르게 하고 싶다면 vi.when 으로 인자 조합별로 다른 동작을 붙일 수 있어요. 직접 if/else 를 쓸 필요 없이요. 자세한 내용은 Conditional Mocking 레시피를 보세요.
:::
목 구현 (Mock Implementation)
고정된 반환값만으론 부족할 때가 있어요. 목이 인자를 받아 실제로 뭔가 하길 원할 때죠. mockImplementation 은 완전한 대체 함수를 제공하게 해줘요:
import { expect, test, vi } from 'vitest'
test('mock with custom implementation', () => {
const add = vi.fn()
add.mockImplementation((a, b) => a + b)
expect(add(1, 2)).toBe(3)
expect(add(10, 20)).toBe(30)
})
단축으로, 구현을 vi.fn() 에 직접 넘길 수도 있어요:
const add = vi.fn((a, b) => a + b)
호출 기록 검사하기
목 함수의 가장 강력한 점 중 하나는 자기에게 걸린 모든 호출을 기억한다는 거예요. 함수가 몇 번 호출됐는지, 어떤 인자를 받았는지, 무엇을 반환했는지를 단언할 수 있어요:
import { expect, test, vi } from 'vitest'
test('inspecting mock calls', () => {
const greet = vi.fn()
greet('Alice')
greet('Bob', 'Charlie')
// Number of calls
expect(greet).toHaveBeenCalledTimes(2)
// Check specific arguments
expect(greet).toHaveBeenCalledWith('Alice')
expect(greet).toHaveBeenCalledWith('Bob', 'Charlie')
// Check the arguments of a specific call by position
expect(greet).toHaveBeenNthCalledWith(1, 'Alice')
expect(greet).toHaveBeenLastCalledWith('Bob', 'Charlie')
// Access the raw call data
expect(greet.mock.calls).toEqual([
['Alice'],
['Bob', 'Charlie'],
])
})
.mock 프로퍼티는 호출 기록 전체에 접근하게 해줘요. .mock.calls 외에 .mock.results 로 각 호출이 무엇을 반환했는지(또는 던졌는지) 볼 수 있어요:
const double = vi.fn(x => x * 2)
double(5)
double(10)
expect(double.mock.results).toEqual([
{ type: 'return', value: 10 },
{ type: 'return', value: 20 },
])
::: warning
.mock.calls 는 인자의 복사본이 아니라 참조를 저장해요. 목에 객체를 넘긴 뒤 그 객체를 변경하면, 기록된 호출도 호출 당시의 상태가 아니라 변경된 상태를 반영해요:
const fn = vi.fn()
const obj = { count: 1 }
fn(obj)
obj.count = 2
// ❌ This fails! mock.calls[0][0].count is now 2, not 1
expect(fn).toHaveBeenCalledWith({ count: 1 })
원래 값을 단언해야 한다면 mockImplementation 으로 호출 시점의 복제본을 잡아두면 돼요:
const calls = []
const fn = vi.fn((obj) => {
calls.push(structuredClone(obj))
})
const obj = { count: 1 }
fn(obj)
obj.count = 2
expect(calls[0]).toEqual({ count: 1 }) // ✅ passes
아니면 객체가 바뀌기 전에 단언을 먼저 하면 됩니다. :::
메서드 감시하기 (Spying on Methods)
vi.spyOn 은 vi.fn() 과 중요한 차이가 있어요. 완전히 새로운 함수를 만드는 대신, 객체의 기존 메서드를 감싸요. 기본적으로 원래 구현은 그대로 동작하지만, 모든 호출을 관찰할 수 있고 선택적으로 동작을 덮어쓸 수도 있어요:
import { expect, test, vi } from 'vitest'
const calculator = {
add(a, b) {
return a + b
},
}
test('spy on a method', () => {
const spy = vi.spyOn(calculator, 'add')
// The original implementation still works
expect(calculator.add(1, 2)).toBe(3)
// But we can observe calls
expect(spy).toHaveBeenCalledWith(1, 2)
expect(spy).toHaveBeenCalledTimes(1)
})
test('spy can override implementation', () => {
const spy = vi.spyOn(calculator, 'add')
spy.mockReturnValue(42)
expect(calculator.add(1, 2)).toBe(42)
})
이건 특히 코드가 메서드를 올바르게 호출하는지, 그 메서드의 동작을 통째로 바꾸지 않고 확인하고 싶을 때 유용해요.
목 초기화하기 (Resetting Mocks)
목 함수는 테스트가 돌면서 상태를 쌓아요. 모든 호출, 모든 반환값, 설정한 커스텀 구현을 기억하죠. 테스트 사이에 초기화하지 않으면 이 상태가 새어 나가서 헷갈리는 실패를 일으킬 수 있어요. Vitest 는 세 단계의 정리 수준을 제공해요:
mockClear()— 기록된 호출 기록과 반환값을 비우되, 설정한 커스텀 구현은 유지mockReset()—mockClear가 하는 모든 일에 더해 커스텀 구현도 제거해서 목을 기본 상태로 되돌림mockRestore()—vi.spyOn으로 만든 스파이 전용. 원래 객체 메서드를 복원해서 스파이를 사실상 되돌림.vi.fn()목에는mockReset과 동일하게 동작
실무에서는 각 테스트 후에 모든 목을 자동으로 복원하는 게 가장 쉽습니다:
import { afterEach, expect, test, vi } from 'vitest'
const calculator = {
add: (a, b) => a + b,
}
afterEach(() => {
vi.restoreAllMocks()
})
test('spy is restored after the test', () => {
const spy = vi.spyOn(calculator, 'add').mockReturnValue(42)
expect(calculator.add(1, 2)).toBe(42)
// afterEach will restore calculator.add to the original implementation
})
더 나아가 restoreMocks 설정으로 전역에 지정하면 afterEach 조차 필요 없어요:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
restoreMocks: true,
},
})
모듈 모킹
단일 함수가 아니라 전체 모듈을 통째로 교체해야 할 때도 있어요. 예를 들어 테스트 중에 실행하고 싶지 않은 데이터베이스 클라이언트나 로거가 그렇죠. vi.mock 은 모듈의 export 를 목 구현으로 바꿔줘요:
import { expect, test, vi } from 'vitest'
import { getUser } from './db.js'
vi.mock(import('./db.js'), () => ({
getUser: vi.fn(),
}))
test('mock a module', () => {
vi.mocked(getUser).mockReturnValue({ name: 'Alice' })
const user = getUser(1)
expect(user.name).toBe('Alice')
expect(getUser).toHaveBeenCalledWith(1)
})
::: warning
vi.mock 호출은 파일 상단으로 호이스팅돼요. 모든 import 보다 먼저 실행되죠. 그래서 테스트 코드가 돌 때는 이미 목 버전이 자리를 잡고 있는 거예요.
:::
::: warning
어떤 경우든 문자열 './db.js' 대신 import('./db.js') 를 넘기는 걸 잊지 마세요. import() 를 쓰면 TypeScript 가 모듈의 타입을 추론할 수 있어서, 팩토리 함수의 반환값이 타입 검사되고 importOriginal 도 올바르게 타입이 지정된 모듈을 반환해요. 덤으로 파일을 IDE 에서 이름을 바꾸거나 이동하면 import 경로도 자동으로 갱신돼요. 문자열을 쓰면 타입 안전성과 자동 리팩터링을 둘 다 잃어버려요.
:::
Vitest 는 구체적인 모킹 시나리오별로 방대한 가이드를 준비해 뒀어요:
- Mocking Functions
- Mocking Modules
- Mocking Timers
- Mocking Dates
- Mocking Globals
- Mocking Requests
- Mocking the File System
- Mocking Classes
더 알아보기 (Learn more)
- Mocking — 모킹 전반의 치트시트·레퍼런스
- vi API —
vi객체의 전체 API - Mock API — 목 함수의 메서드 레퍼런스
- Snapshot Testing — 다음 단계로 스냅샷 테스트