모의화
테스트를 작성하다 보면 내부 또는 외부 서비스의 "가짜" 버전을 만들어야 할 때가 있습니다. 이를 일반적으로 **모의화(mocking)**라고 합니다. Vitest는 vi 도우미를 통해 이를 돕는 유틸리티 함수를 제공합니다. import { vi } from 'vitest'
를 사용하여 가져오거나, 전역으로 접근할 수 있습니다 ( 전역 구성이 활성화된 경우).
WARNING
각 테스트 실행 전후에 모의화(mock)를 항상 초기화하거나 복원하여 테스트 간의 모의화 상태 변경을 방지해야 합니다! 자세한 내용은 mockReset
문서를 참조하십시오.
빠르게 시작하려면 API 섹션을 확인하고, 모의화에 대해 자세히 알아보세요.
날짜
테스트 시 일관성을 유지하기 위해 날짜를 제어해야 할 경우가 있습니다. Vitest는 타이머와 시스템 날짜를 조작하기 위해 @sinonjs/fake-timers
패키지를 사용합니다. 특정 API에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
예시
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const businessHours = [9, 17];
function purchase() {
const currentHour = new Date().getHours();
const [open, close] = businessHours;
if (currentHour > open && currentHour < close) return { message: 'Success' };
return { message: 'Error' };
}
describe('purchasing flow', () => {
beforeEach(() => {
// vitest에게 모의 시간 사용을 알립니다.
vi.useFakeTimers();
});
afterEach(() => {
// 각 테스트 실행 후 날짜를 복원합니다.
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// 영업 시간 내로 시간을 설정합니다.
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// Date.now()에 접근하면 위에서 설정한 날짜가 반환됩니다.
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// 영업 시간 외로 시간을 설정합니다.
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// Date.now()에 접근하면 위에서 설정한 날짜가 반환됩니다.
expect(purchase()).toEqual({ message: 'Error' });
});
});
함수
함수 모의화는 스파이와 모의 함수, 두 가지 범주로 나눌 수 있습니다.
특정 함수가 호출되었는지 (그리고 어떤 인수가 전달되었는지) 확인하기만 하면 되는 경우가 있습니다. 이러한 경우 스파이만으로 충분하며, vi.spyOn()
을 사용하여 직접 생성할 수 있습니다 (자세한 내용은 여기 참조).
그러나 스파이는 함수를 감시하는 데만 사용되며, 해당 함수의 구현을 변경할 수는 없습니다. 함수의 가짜 (또는 모의화된) 버전을 만들어야 하는 경우 vi.fn()
을 사용할 수 있습니다 (자세한 내용은 여기 참조).
함수 모의화의 기반으로 Tinyspy를 사용하지만, jest
와 호환성을 위해 자체 래퍼를 가지고 있습니다. vi.fn()
과 vi.spyOn()
은 동일한 메서드를 공유하지만, vi.fn()
의 반환 값만 호출할 수 있습니다.
예시
import { afterEach, describe, expect, it, vi } from 'vitest';
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // getter 또는 setter도 지원됩니다.
};
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest');
expect(spy.getMockName()).toEqual('getLatest');
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1]
);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementationOnce(() => 'access-restricted');
expect(messages.getLatest()).toEqual('access-restricted');
expect(spy).toHaveBeenCalledTimes(2);
});
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(1);
mock.mockImplementationOnce(() => 'access-restricted');
expect(mock()).toEqual('access-restricted');
expect(mock).toHaveBeenCalledTimes(2);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(3);
});
});
더 보기
전역 변수
vi.stubGlobal
헬퍼를 사용하여 jsdom
또는 node
에 없는 전역 변수를 모의화할 수 있습니다. 전역 변수의 값을 globalThis
객체에 할당합니다.
import { vi } from 'vitest';
const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
// 이제 `IntersectionObserver` 또는 `window.IntersectionObserver`로 접근할 수 있습니다.
모듈
모듈 모의화는 다른 코드에서 호출되는 타사 라이브러리를 감시하여 인수, 출력 또는 구현을 재정의할 수 있도록 합니다.
자세한 API 설명은 vi.mock()
API 섹션을 참조하십시오.
자동 모의화 알고리즘
코드에서 연결된 __mocks__
파일이나 해당 모듈에 대한 factory
없이 모의화된 모듈을 가져오는 경우, Vitest는 모듈을 호출하고 모든 내보내기를 모의화하여 모듈 자체를 자동으로 모의화합니다.
다음 규칙이 적용됩니다.
- 모든 배열은 비워집니다.
- 모든 원시 타입 및 컬렉션은 동일하게 유지됩니다.
- 모든 객체는 깊은 복사됩니다.
- 클래스의 모든 인스턴스와 해당 프로토타입은 깊은 복사됩니다.
예시
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// handlers
export function success(data) {}
export function failure(data) {}
// get todos
export async function getTodos(event, context) {
const client = new Client({
// ...clientOptions
});
await client.connect();
try {
const result = await client.query('SELECT * FROM todos;');
client.end();
return success({
message: `${result.rowCount} item(s) returned`,
data: result.rows,
status: true,
});
} catch (e) {
console.error(e.stack);
client.end();
return failure({ message: e, status: false });
}
}
vi.mock('pg', () => {
const Client = vi.fn();
Client.prototype.connect = vi.fn();
Client.prototype.query = vi.fn();
Client.prototype.end = vi.fn();
return { Client };
});
vi.mock('./handlers.js', () => {
return {
success: vi.fn(),
failure: vi.fn(),
};
});
describe('get a list of todo items', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return items successfully', async () => {
client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(success).toBeCalledWith({
message: '0 item(s) returned',
data: [],
status: true,
});
});
it('should throw an error', async () => {
const mError = new Error('Unable to retrieve rows');
client.query.mockRejectedValueOnce(mError);
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(failure).toBeCalledWith({ message: mError, status: false });
});
});
요청
Vitest는 Node 환경에서 실행되므로 네트워크 요청을 모의화하기가 어려울 수 있습니다. 웹 API를 사용할 수 없으므로 네트워크 동작을 모방할 수 있는 도구가 필요합니다. 이러한 이유로 Mock Service Worker를 사용하는 것을 권장합니다. 이를 통해 REST 및 GraphQL 네트워크 요청을 모두 모의화할 수 있으며, 특정 프레임워크에 종속되지 않습니다.
Mock Service Worker (MSW)는 테스트 과정에서 발생하는 요청을 가로채 애플리케이션 코드 수정 없이 모의화 기능을 제공합니다. 브라우저에서는 Service Worker API를 사용하고, Node.js 및 Vitest 환경에서는 node-request-interceptor를 활용합니다. MSW에 대한 자세한 내용은 소개를 참조하십시오.
구성
설정 파일에서 다음과 같이 설정하여 사용할 수 있습니다.
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, rest } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
rest.get('https://rest-endpoint.example/path/to/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(posts));
}),
];
const graphqlHandlers = [
graphql.query(
'https://graphql-endpoint.example/api/v1/posts',
(req, res, ctx) => {
return res(ctx.data(posts));
}
),
];
const server = setupServer(...restHandlers, ...graphqlHandlers);
// 모든 테스트 전에 서버를 시작합니다.
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// 모든 테스트 후에 서버를 닫습니다.
afterAll(() => server.close());
// 각 테스트 후 핸들러를 초기화합니다. (테스트 격리를 위해 중요합니다.)
afterEach(() => server.resetHandlers());
onUnhandleRequest: 'error'
설정을 통해 서버에 정의되지 않은 요청이 발생할 경우 오류를 발생시킬 수 있습니다.
예시
MSW를 활용한 완전한 예제는 다음 링크에서 확인할 수 있습니다. MSW를 사용한 React 테스팅.
더 보기
MSW에는 훨씬 더 많은 기능이 있습니다. 쿠키, 쿼리 매개변수 접근, 모의 오류 응답 정의 등 다양한 기능을 제공합니다! MSW로 할 수 있는 모든 것을 보려면 해당 문서를 참조하십시오.
타이머
시간 제한 또는 간격이 포함된 코드를 테스트할 때, 테스트가 완료될 때까지 기다리거나 시간 초과를 발생시키는 대신, setTimeout
및 setInterval
호출을 모의화하는 '가짜' 타이머를 사용하여 테스트 속도를 향상시킬 수 있습니다.
자세한 API 설명은 vi.useFakeTimers
API 섹션을 참조하십시오.
예시
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 hours
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minute
}
const mock = vi.fn(() => console.log('executed'));
describe('delayed execution', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should execute the function', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should not execute the function', () => {
executeAfterTwoHours(mock);
// 2ms만큼 시간을 진행해도 함수가 실행되지 않습니다.
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('should execute every minute', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Cheat Sheet
INFO
아래 예제의 vi
는 vitest
에서 직접 가져온 것입니다. 구성에서 globals
를 true
로 설정하면 전역으로 사용할 수도 있습니다.
다음을 수행하고 싶습니다…
method
스파이하기
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- 내보낸 변수 모킹하기
// some-path.js
export const getter = 'variable';
// some-path.test.ts
import * as exports from './some-path.js';
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');
- 내보낸 함수 모킹하기
vi.mock
을 사용한 예시:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
vi.mock
호출은 파일 상단으로 호이스팅되므로 beforeEach
내부에 넣지 마세요. beforeEach
내부에 둘 이상의 vi.mock
호출이 있을 경우, 그 중 하나만 모듈을 모킹합니다.
vi.spyOn
을 사용한 예:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- 내보낸 클래스 구현 모킹하기
vi.mock
및 프로토타입을 사용한 예:
// some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances는 SomeClass의 인스턴스를 갖습니다.
vi.mock
및 반환 값을 사용한 예:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns는 반환된 객체들을 갖습니다.
vi.spyOn
을 사용한 예:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// 처음 두 예제 중 원하는 방식으로 구현할 수 있습니다.
});
- 함수에서 반환된 객체 스파이하기
캐시를 사용한 예:
// some-path.ts
export function useObject() {
return { method: () => true };
}
// useObject.js
import { useObject } from './some-path.js';
const obj = useObject();
obj.method();
// useObject.test.js
import { useObject } from './some-path.js';
vi.mock('./some-path.js', () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// 이제 useObject()가 호출될 때마다
// 동일한 객체 레퍼런스를 반환합니다.
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method는 some-path 내부에서 호출되었습니다.
expect(obj.method).toHaveBeenCalled();
- 모듈의 일부만 모킹하기
import { mocked, original } from './some-path.js';
vi.mock('./some-path.js', async () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./some-path.js'
);
return {
...mod,
mocked: vi.fn(),
};
});
original(); // 원본 동작을 유지합니다.
mocked(); // 스파이 함수로 모킹되었습니다.
- 현재 날짜 모킹하기
Date
의 시간을 모의하려면 vi.setSystemTime
도우미 함수를 사용할 수 있습니다. 이 값은 테스트 간에 자동으로 재설정되지 않습니다.
vi.useFakeTimers
를 사용하면 Date
의 시간도 변경된다는 점에 유의하세요.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// 모킹된 시간 초기화
vi.useRealTimers();
- 전역 변수 모킹하기
globalThis
에 값을 할당하거나 vi.stubGlobal
도우미를 사용하여 전역 변수를 설정할 수 있습니다. vi.stubGlobal
을 사용하는 경우 unstubGlobals
구성 옵션을 활성화하거나 vi.unstubAllGlobals
를 호출하지 않는 한 테스트 간에 자동으로 재설정되지 않습니다.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
import.meta.env
모킹하기
환경 변수를 변경하려면 새 값을 할당하기만 하면 됩니다. 이 값은 테스트 간에 자동으로 재설정되지 않습니다.
import { beforeEach, expect, it } from 'vitest';
// beforeEach 훅에서 수동으로 초기화할 수 있습니다.
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('changes value', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
값을 자동으로 재설정하려면 unstubEnvs
구성 옵션을 활성화한 상태에서 vi.stubEnv
도우미를 사용하거나 beforeEach
훅에서 vi.unstubAllEnvs
를 수동으로 호출할 수 있습니다.
import { expect, it, vi } from 'vitest';
// 테스트 실행 전 "VITE_ENV"는 "test"입니다.
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('다른 테스트를 실행하기 전에 값이 복원됩니다.', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};