JavaScript / TypeScript 기반의 코드를 작성하다보면 vitest나 jest라는 테스팅 라이브러리의 도움을 받게 됩니다.
이 때, 자연스럽게 `fn()`, `spyOn()`, `mock()` 등의 API를 활용하여 테스트 더블을 만들어줍니다.
다음 테스트 케이스를 실행하기 전에, 현재 테스트 케이스에서 사용했던 Mock을 정리해주는 것이 좋습니다.
왜냐하면 해당 Mock이 다음 테스트 케이스에 영향을 줄 수 있기 때문입니다.
테스트를 수행하기 위해서는 잘 Mocking하는 것도 중요하지만 Mock을 깨끗이 초기화시키는 것 또한 중요합니다
검증해보기
const consoleLog = console.log;
test("spyOn method로 console.log를 모킹하면 console.log는 다른 함수가 된다.", () => {
jest.spyOn(console, "log").mockImplementation(() => {});
const consoleLogMock = console.log;
expect(consoleLogMock).not.toBe(consoleLog); // true
});
테스트 코드를 해석해보도록 합시다.
`console.log` 함수를 담고 있는 `consoleLog`와 `console.log`의 모킹객체인 `consoleLogMock`을 테스트해보면
둘은 서로 같지 않음을 증명할 수 있게 됩니다.
그렇다면 테스트를 정리하는 방법은 어떤 것이 있을까요?
테스트를 정리하는 방법
1. 테스트 코드에서 수동으로 mock 정리하기
2. jest의 설정 파일을 활용하여 자동으로 정리하기
1) 수동으로 정리하기
(1) mock 객체마다 정리하기
1. `mockFn.mockClear()`
`mockFn.mock.calls`와 `mockFn.mock.instances` 배열을 초기화하며,
다음 테스트케이스를 실행하기 전에 mock 함수를 호출했던 정보를 비우고 싶을 때 유용합니다.
`mockFn.mock.calls`: `mockFn`함수가 호출되었을 경우의 매개변수 목록들이 들어있음 (2차원 배열로 들어있음)
`mockFn.mock.instances`: `mockFn` 함수가 생성자로 호출되었을 경우에 생성했던 인스턴스 객체의 목록이 들어 있음 (1차원 배열 형태로 들어있음)
describe("clearMock 테스트", () => {
describe("mock 함수를 초기화한 후, mockClear를 초기화하면", () => {
it("mock.calls는 초기화됨", () => {
const mockFn = jest.fn().mockReturnValue(42);
mockFn("1");
mockFn("1", "2");
expect(mockFn.mock.calls[0]).toEqual(["1"]);
expect(mockFn.mock.calls[1]).toEqual(["1", "2"]);
mockFn.mockClear();
expect(mockFn.mock.calls).toEqual([]);
});
it("내부 구현은 초기화 되지 않음", () => {
const mockFn = jest.fn().mockReturnValue(42);
mockFn("1");
mockFn.mockClear();
expect(mockFn()).toBe(42);
});
it("mock.instances는 초기화됨", () => {
const MockClass = jest.fn();
const a = new MockClass();
const b = new MockClass();
expect(MockClass.mock.instances).toHaveLength(2);
expect(MockClass.mock.instances).toEqual([a, b]);
MockClass.mockClear();
expect(MockClass.mock.instances).toHaveLength(0);
});
});
});
테스트 코드를 보면 내부 구현은 변경되지 않고, 호출 횟수, 생성된 인스턴스만을 초기화할 수 있습니다.
즉, 테스트 내부적으로 공통적인 로직을 테스트하고 싶다면 `mockClear`를 활용하는 것이 좋습니다.
2. `mockFn.mockReset()`
내부적으로 `mockClear`를 호출하며, mock 함수의 구현(Implementation)을 undefined를 반환하는 빈 함수로 초기화합니다.
즉, `mockClear`보다 더 강력한 초기화를 하게 됩니다 (호출 횟수, 호출 매개변수, 생성된 인스턴스, 함수 구현)
describe("mockReset 테스트", () => {
describe("mock 함수를 초기화한 후, mockReset을 초기화하면", () => {
it("mock.calls는 초기화됨", () => {
const mockFn = jest.fn().mockReturnValue(42);
mockFn("1");
mockFn("1", "2");
expect(mockFn.mock.calls[0]).toEqual(["1"]);
expect(mockFn.mock.calls[1]).toEqual(["1", "2"]);
mockFn.mockReset();
expect(mockFn.mock.calls).toEqual([]);
});
it("내부 구현은 초기화 됨", () => {
const mockFn = jest.fn().mockReturnValue(42);
mockFn("1");
mockFn.mockReset();
expect(mockFn()).toBeUndefined();
});
it("mock.instances는 초기화됨", () => {
const MockClass = jest.fn();
const a = new MockClass();
const b = new MockClass();
expect(MockClass.mock.instances).toHaveLength(2);
expect(MockClass.mock.instances).toEqual([a, b]);
MockClass.mockReset();
expect(MockClass.mock.instances).toHaveLength(0);
});
});
});
3. `mockFn.mockRestore()`
내부적으로 `mockReset`을 호출하며, `mock`하지 않았던 원래의 구현도 복원을 할 수 있습니다.
다만 `spyOn()`으로 만들어진 mock에만 동작합니다. 코드를 보며 더 자세한 예시를 들어봅시다.
const someModule = { api: () => "original" };
it("spyOn으로 테스트 더블을 만든 뒤엔, someModule.api는 다른 함수가 되어버린다", () => {
const originApi = someModule.api;
// 여기서 바뀌어 버림
const mockApi = jest
.spyOn(someModule, "api")
.mockImplementation(() => "mock");
// mockApi.mockRestore(); // 이게 실행되고 아니고가 다음 테스트에 영향을 줌
const changeApi = someModule.api;
expect(originApi).not.toBe(changeApi);
expect(changeApi()).toBe("mock");
});
`spyOn`함수는 객체의 메서드를 모킹하고 싶을 경우에 유용하지만 테스트 케이스를 실행하고 나면 someModule.api는 다른 함수가 되어 버립니다.
const anotherModule = { api: () => "original" };
it("spyOn으로 테스트 더블을 만든 뒤에 mockRestore를 호출하면, 원래 함수로 복원됨", () => {
const originApi = anotherModule.api;
const mockApi = jest
.spyOn(anotherModule, "api")
.mockImplementation(() => "mock");
expect(mockApi()).toBe("mock");
mockApi.mockRestore();
expect(mockApi()).toBeUndefined();
const changeApi = anotherModule.api;
expect(originApi).toBe(changeApi);
expect(changeApi()).toBe("original");
});
`spyOn`으로 모킹한 객체를 원래대로 초기화하려면 `mockRestore`를 초기화해야 합니다.
(2) jest 객체로 모든 mock을 정리하기
`jest.clearAllMocks`, `jest.resetAllMocks`, `jest.restoreAllMocks`를 활용하면 `mockFn`마다 mock을 정리하는 수고를 줄일 수 있습니다.
- `jest.clearAllMocks`: 모든 mock 함수에서 `mockFn.clearAllMocks`를 호출합니다.
- `jest.resetAllMocks`: 모든 mock 함수에서 `mockFn.resetAllMocks`를 호출합니다.
- `jest.restoreAllMocks`: 모든 mock 함수에서 `mockFn.restoreAllMocks`를 호출합니다.
2) 자동으로 정리하기
원하는 초기화의 수준에 따라 jest.config.js의 clearMocks, resetMocks, restoreMocks 설정을 원하는 수준에 따라 true로 변경하면 됩니다.
지금까지 배운 것들을 토대로 처음에 봤던 검증하기 부분의 `console.log`를 초기화를 시키려면 어떻게 해야할까요?
바로 `restoreAllMocks`을 활용해야 합니다.
const consoleLog = console.log;
test("spyOn method로 console.log를 모킹하면 consoel.ㅣlog는 다른 함수가 된다.", () => {
jest.spyOn(console, "log").mockImplementation(() => {});
const consoleLogMock = console.log;
expect(consoleLogMock).not.toBe(consoleLog);
jest.restoreAllMocks();
const consoleLogAfterRestore = console.log;
expect(consoleLogAfterRestore).toBe(consoleLog);
});