"Mock 은 자기가 호출됐다는 걸 기억하는 함수야."
vi.fn() — 빌딩 블록
vi.fn() 은 새 mock 함수를 반환해. Default 로 모든 호출에 undefined 반환하지만, 모든 호출을 inspect 가능한 .mock 객체에 기록해 — 인자, 반환값, this 바인딩까지. 그 기록이 이걸 mock 으로 만들어.
세 가족의 메서드로 구성해:
반환값 : 매 호출 mockReturnValue(x), 다음 호출만 mockReturnValueOnce(x) (queue 식).
비동기 반환 : mockResolvedValue(x), mockRejectedValue(err) — Promise 모양 반환의 sugar.
완전한 구현 : mockImplementation(fn) — mock 이 입력 기반으로 뭘 계산해야 할 때.
일어난 일 inspect 하기
대상 코드 돌고 나면 기록에 단언해. 지배적인 두 패턴:
expect(mock).toHaveBeenCalled() / toHaveBeenCalledTimes(n) — 호출됐어? 몇 번?
expect(mock).toHaveBeenCalledWith(args) / toHaveBeenLastCalledWith(args) — 어떤 인자 봤어?
더 외과적인 inspection 은 mock.calls 에 직접 손 넣어 — 호출당 한 항목, 인자 배열의 배열이야.
vi.spyOn — 함수가 이미 있을 때
vi.fn() 은 무에서 함수를 만들어. vi.spyOn(obj, 'method') 은 객체에 이미 있는 메서드를 wrap. Wrap 된 버전은 default 로 여전히 원본 호출하지만, 모든 호출이 기록되고, 필요하면 구현 오버라이드도 가능해.
킬러 feature 는 복원 . spy.mockRestore() 가 원본 메서드를 다시 끼워. afterEach(() => vi.restoreAllMocks()) 와 짝지으면 다른 테스트로 spy 안 새.
관찰하되 교체 안 하려면 `vi.spyOn`. 의존성으로 넘길 새 fake 가 필요하면 `vi.fn()`. 둘 다 작동하는 경우에도 정신은 호환되지 않아 — 선택이 독자에게 의도를 신호해.
세 reset 메서드 (관례 하나 골라)
Vitest 가 비슷해 보이지만 다른 세 cleanup 메서드 줘:
mockClear() — 호출 이력 (mock.calls, mock.results) 지움. 구현과 반환값 queue 는 유지.
mockReset() — mockClear() + 구현을 default (undefined 반환) 로 reset.
mockRestore() — spy 에만; 원본 메서드 복원. mockReset() 함의.
가장 깔끔한 관례: 모든 테스트 파일 맨 위에 (또는 vitest.setup.ts 에 전역으로) afterEach(() => vi.restoreAllMocks()) 두고, 수동 cleanup 신경 끊어. config 옵션 restoreMocks: true 가 같은 걸 암묵적으로 해줘.
Mock 은 default 로 새. 복원 잊은 spy 는 테스트 프로세스 나머지 동안 붙어있어. 같은 파일의 다음 테스트가 진짜 메서드 말고 spy 된 메서드에 대해 돌고, 너는 완전히 올바른 함수가 왜 undefined 반환하는지 디버깅하는 데 오후 하나 써. cleanup 은 프로젝트 레벨에서 한 번 세팅.
Code vi.fn() — 기록, 구성, inspect· typescript import { describe, it, expect, vi } from 'vitest';
describe('vi.fn() basics', () => {
it('records every call', () => {
const handler = vi.fn();
handler('hello');
handler('world', { important: true });
expect(handler).toHaveBeenCalledTimes(2);
expect(handler).toHaveBeenCalledWith('hello');
expect(handler).toHaveBeenLastCalledWith('world', { important: true });
// Surgical inspection
expect(handler.mock.calls[0]).toEqual(['hello']);
expect(handler.mock.calls[1][1].important).toBe(true);
});
it('can return configured values', () => {
const getUser = vi.fn()
.mockReturnValueOnce({ id: 1 })
.mockReturnValueOnce({ id: 2 })
.mockReturnValue({ id: 'fallback' });
expect(getUser()).toEqual({ id: 1 });
expect(getUser()).toEqual({ id: 2 });
expect(getUser()).toEqual({ id: 'fallback' });
expect(getUser()).toEqual({ id: 'fallback' });
});
it('can model async behavior', async () => {
const fetchUser = vi.fn().mockResolvedValue({ name: 'Pippa' });
await expect(fetchUser(1)).resolves.toEqual({ name: 'Pippa' });
});
});vi.spyOn — wrap, 관찰, 복원· typescript import { describe, it, expect, vi, afterEach } from 'vitest';
afterEach(() => {
vi.restoreAllMocks(); // single line, never leak
});
describe('vi.spyOn — observe without losing the original', () => {
it('lets you check that console.warn was called', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
callCodeThatWarns();
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
// After the test, restoreAllMocks puts console.warn back to normal.
});
it('can replace the method while still recording calls', () => {
const fetch = vi.spyOn(global, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ ok: true }), { status: 200 })
);
return fetchUserById(42).then((user) => {
expect(user.ok).toBe(true);
expect(fetch).toHaveBeenCalledWith(expect.stringContaining('/users/42'));
});
});
});프로젝트 전체 cleanup config — 한 번 세팅, 영원히 안 샘· typescript // In vitest.config.ts — set restoreMocks once, forget cleanup forever
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['./vitest.setup.ts'],
restoreMocks: true, // auto-restore spies between tests
clearMocks: true, // auto-clear call history between tests
// unstubGlobals: true, // also unstub vi.stubGlobal calls
},
});Exercise notify(user, channel) 함수 짜 — channel 인자 기반으로 sendEmail(user.email, msg), sendSMS(user.phone, msg), sendPush(user.deviceId, msg) 중 하나 호출. vi.fn() 으로 각 sender 만들어 세 번 테스트 — 맞는 sender 가 맞는 인자로 호출됐고, 나머지는 호출 안 됐는지 (toHaveBeenCalledTimes(0)) 단언. 보너스: 별개 vi.fn 대신 세 sender 다 가진 객체에 vi.spyOn 써봐.
Hint 한 큰 테스트에서 if (channel === 'email') expect(sendEmail).toHaveBeenCalled() 식으로 쓰고 있으면, 세 테스트로 쪼개. 각 테스트가 한 채널 이름 짓고 계약 단언.