"vi.mock 은 너의 import 보다 먼저 돌아. 그거 이해하면 나머지는 기계적이야."
왜 module mock 이 존재해
가짜로 만들고 싶은 의존성이 인자로 넘길 수 있는 게 아닐 때가 있어 — 대상 코드가 직접 import 하는 모듈이야. fetchUserById 가 ./api 를 import 하고, 그게 fetch import 해서, 진짜 네트워크와 통신해. 다섯 층 통해 fake 를 꿰지 않고 "이 테스트 파일에서 `./api` 를 교체해" 라고 말하고 싶지.
vi.mock('./api') 가 그걸 해. Factory 없으면 모든 export 를 auto-mock 으로 교체 (각 함수가 vi.fn() 이 됨). Factory 있으면 교체를 명시적으로 제어.
Hoisting 함정
Vitest 가 로드 타임에 테스트 파일을 transform 하면서 모든 top-level vi.mock 호출을 맨 위로 — 어떤 import 보다도 앞으로 — 물리적으로 옮겨. import 가 진짜 모듈 말고 mock 을 봐야 하니까 필요해. 대가는 vi.mock 에 넘긴 factory function 이 파일의 모든 코드보다 먼저 돈다는 거. 테스트 파일의 변수, 헬퍼, 상수 참조 불가. 아직 존재하지 않거든.
Mock factory 와 테스트 본문 사이 데이터 공유가 필요하면 — 예를 들어 나중에 inspect 할 captured spy — vi.hoisted 로 그 데이터를 mock 옆에, 같이 hoist 되게, 같이 일찍 선언해.
`vi.mock` 안에서 테스트 파일 변수 close 하면 혼란스러운 에러로 실패해. 메시지가 보통 "Cannot access 'foo' before initialization" 같이 나와 — hoisted factory 가 아직 bind 안 된 이름에 손 뻗는 거야. 해법은 `vi.hoisted` 지 beforeAll 아냐.
부분 mock — 대부분 유지, 일부 교체
모듈에 export 열 개 있는데 하나만 교체하고 싶으면, factory 안에서 vi.importActual 써. 원본 모듈 반환해 — spread 하고 필요한 것만 오버라이드. 유틸 모듈에 맞는 움직임이야 — 다른 14 함수 그대로 두고 now() 만 교체.
테스트별 오버라이드 `mockResolvedValueOnce`
Module mock 은 default 로 파일 scope — 파일의 모든 테스트에 적용. 테스트마다 다른 반환값 필요하면 mock 된 함수 (이제 vi.fn()) 가 평소 queue 메서드 지원해. vi.mocked(realImport) 로 타입된 참조 얻고 타입 체커가 도와줄 거야.
Mock 항상 타입 지정.vi.mocked(fetchUser).mockResolvedValueOnce({ id: 1 }) 가 반환 모양에 자동완성 줘. 타입 없는 mock 은 쓰레기 반환을 조용히 허용해, 테스트가 버그 잡는 대신 쓰레기에 대해 통과해.
Code
대상 코드 — API 모듈을 import 하는 서비스·typescript
// src/services/api.ts
export async function fetchUserById(id: number) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`User ${id} not found`);
return response.json();
}
// src/services/user-service.ts
import { fetchUserById } from './api';
export async function getDisplayName(id: number) {
const user = await fetchUserById(id);
return `${user.firstName} ${user.lastName}`;
}
Auto-mock + 타입 접근 `vi.mocked`·typescript
import { describe, it, expect, vi } from 'vitest';
import { getDisplayName } from './user-service';
import { fetchUserById } from './api';
// Hoisted to the top — runs before the imports above.
vi.mock('./api', () => ({
fetchUserById: vi.fn(),
}));
// Get a typed handle to the mock.
const mockedFetch = vi.mocked(fetchUserById);
describe('getDisplayName', () => {
it('combines first and last name', async () => {
mockedFetch.mockResolvedValueOnce({ firstName: 'Pippa', lastName: 'Choi' });
await expect(getDisplayName(1)).resolves.toBe('Pippa Choi');
expect(mockedFetch).toHaveBeenCalledWith(1);
});
it('propagates fetch errors', async () => {
mockedFetch.mockRejectedValueOnce(new Error('User 999 not found'));
await expect(getDisplayName(999)).rejects.toThrow('User 999 not found');
});
});
부분 mock — `importOriginal` 로 나머지 유지·typescript
// Partial mock — replace one export, keep the rest
import { describe, it, expect, vi } from 'vitest';
import { formatTimestamp, now } from './time-utils';
vi.mock('./time-utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./time-utils')>();
return {
...actual,
now: vi.fn(() => new Date('2026-01-01T00:00:00Z').getTime()),
};
});
describe('formatTimestamp', () => {
it('uses the mocked now() but real formatter', () => {
// formatTimestamp uses now() internally, which is mocked.
// The actual formatter logic still runs.
expect(formatTimestamp()).toBe('2026-01-01T00:00:00.000Z');
});
});
`vi.hoisted` — 공유 데이터의 escape hatch·typescript
// The hoisting trap and its fix
import { describe, it, expect, vi } from 'vitest';
// ❌ This fails — `mockUser` doesn't exist when the factory runs.
// const mockUser = { id: 1, name: 'Pippa' };
// vi.mock('./api', () => ({
// fetchUserById: vi.fn().mockResolvedValue(mockUser), // ReferenceError
// }));
// ✅ Use vi.hoisted to declare data that the factory can reference.
const { mockUser, mockFetch } = vi.hoisted(() => ({
mockUser: { id: 1, name: 'Pippa' },
mockFetch: vi.fn().mockResolvedValue({ id: 1, name: 'Pippa' }),
}));
vi.mock('./api', () => ({
fetchUserById: mockFetch,
}));
describe('with vi.hoisted', () => {
it('shares data between the mock factory and the test', async () => {
expect(mockFetch).toBeDefined();
expect(mockUser.id).toBe(1);
});
});
getUserGreeting(id) 함수 짜 — ./api 의 fetchUserById(id) 호출해서 Hello, ${user.firstName}! 반환. 모듈을 두 가지 방식으로 mock 해: (1) auto-mock + 테스트별 mockResolvedValueOnce, (2) id 기반 커스텀 구현 반환하는 factory (id === 1 은 Pippa, id === 2 는 Dad). 둘 다 돌려서 통과 보고, 일부러 closure-over-variable 버그 도입해서 hoisting 에러 봐.
Hint
에러 메시지가 'Cannot access X before initialization' 이면 테스트 파일 scope 의 뭔가를 close 한 거야. 값을 factory 안으로 인라인하거나, vi.hoisted 로 옮겨.
Progress
Progress is local-only — sign in to sync across devices.