Jest 수동 모킹
Jest 수동 모킹 (Manual Mocks)
테스트에서 원격 리소스(웹사이트나 데이터베이스)에 매번 접근하면 느리고 불안정해져요. 수동 mock은 그런 기능을 가짜 데이터로 대체해서, 테스트를 빠르고 안정적으로 만들어 줘요. 이 페이지에서는 __mocks__ 폴더를 이용한 수동 mock 작성법을 다뤄요.
수동 mock이란
수동 mock은 모듈과 인접한 __mocks__/ 하위 디렉토리에 모듈을 작성하는 방식으로 정의돼요. 예를 들어 models 디렉토리의 user 모듈을 mock하려면 models/__mocks__ 폴더에 user.js 파일을 만들어요.
주의할 점이 몇 가지 있어요.
__mocks__폴더는 대소문자를 구분해요.__MOCKS__라고 이름을 지으면 일부 시스템에서 깨져요.- 테스트에서 그 모듈을 require할 때(수동 mock을 실제 구현 대신 쓰고 싶다는 뜻)
jest.mock('./moduleName')을 명시적으로 호출하는 것이 필수예요. - Node 모듈(
lodash등)을 mock할 때는node_modules옆__mocks__폴더에 두면 자동으로 mock돼요. 명시적 호출이 필요 없어요. - 스코프 모듈(
@scope/project-name)은__mocks__/@scope/project-name.js처럼 스코프 이름에 맞는 디렉토리 구조로 만들면 mock할 수 있어요. fs,path같은 Node 내장 모듈은 기본적으로 mock되지 않으니jest.mock('path')같은 명시 호출이 필수예요.
.
├── config
├── __mocks__
│ └── fs.js
├── models
│ ├── __mocks__
│ │ └── user.js
│ └── user.js
├── node_modules
└── views
예제: fs 모듈 수동 mock
특정 디렉토리의 모든 파일 요약을 제공하는 모듈을 예로 들어요. 실제로 디스크에 접근하지 않도록 fs의 수동 mock을 만들어요. jest.createMockFromModule('fs')로 자동 mock을 만든 뒤 원하는 동작을 덮어써요.
// __mocks__/fs.js
'use strict';
const path = require('path');
const fs = jest.createMockFromModule('fs');
// 테스트가 셋업 중에 "mock 파일시스템"의 모습을 지정할 수 있는 커스텀 함수예요.
let mockFiles = Object.create(null);
function __setMockFiles(newMockFiles) {
mockFiles = Object.create(null);
for (const file in newMockFiles) {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
mockFiles[dir] = [];
}
mockFiles[dir].push(path.basename(file));
}
}
// __setMockFiles로 지정한 mock 파일 목록에서 읽는 readdirSync의 커스텀 버전
function readdirSync(directoryPath) {
return mockFiles[directoryPath] || [];
}
fs.__setMockFiles = __setMockFiles;
fs.readdirSync = readdirSync;
module.exports = fs;
fs는 Node 내장 모듈이므로 테스트에서 jest.mock('fs')를 명시적으로 호출해야 해요.
// __tests__/FileSummarizer-test.js
'use strict';
jest.mock('fs');
describe('listFilesInDirectorySync', () => {
const MOCK_FILE_INFO = {
'/path/to/file1.js': 'console.log("file1 contents");',
'/path/to/file2.txt': 'file2 contents',
};
beforeEach(() => {
// 각 테스트 전에 mock 파일 정보를 셋업해요.
require('fs').__setMockFiles(MOCK_FILE_INFO);
});
test('includes all files in the directory in the summary', () => {
const FileSummarizer = require('../FileSummarizer');
const fileSummary = FileSummarizer.summarizeFilesInDirectorySync('/path/to');
expect(fileSummary.length).toBe(2);
});
});
이 예제는 jest.createMockFromModule로 자동 mock을 만들고 기본 동작을 덮어쓰는 권장 방식이에요. 완전히 수동으로 하고 싶다면 mock 파일에서 직접 함수를 export 해도 돼요. 다만 완전 수동 mock은 mock 대상 모듈이 바뀔 때마다 직접 업데이트해야 한다는 단점이 있어요.
실제 구현과 동기화 유지하기
수동 mock과 실제 구현이 영영 어긋나지 않도록, 수동 mock 안에서 jest.requireActual(moduleName)으로 실제 모듈을 require하고 mock 함수로 덧붙이는 방식이 유용해요.
const actual = jest.requireActual('fs');
ES 모듈과 jes.mock 호이스팅
ES 모듈 import를 쓰면 import 문을 파일 맨 위에 두고 싶어지죠. 하지만 종종 모듈이 사용되기 전에 mock을 지시해야 해요. Jest는 jest.mock 호출을 모듈 맨 위(모든 import 앞)로 자동 호이스팅해요.
주의: ECMAScript modules 지원을 켜면 jest.mock은 맨 위로 호이스팅될 수 없어요. ESM 로더는 코드 실행 전에 항상 정적 import를 평가하기 때문이에요. ECMAScriptModules를 참고해요.
JSDOM 미구현 메서드 mock하기
Jest가 쓰는 DOM 구현 JSDOM에서 아직 구현 안 된 메서드를 코드가 쓰면 테스트가 어려워져요. 예를 들어 window.matchMedia()는 TypeError: window.matchMedia is not a function을 만들고 테스트가 실행되지 않아요.
테스트 파일에서 matchMedia를 mock하면 해결돼요.
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
만약 window.matchMedia()가 테스트에서 호출되는 함수 안에서 쓰인다면 위 코드로 충분해요. 그런데 테스트 파일 자체(최상위)에서 직접 실행된다면 별도 파일로 분리해 테스트 대상 파일 앞에 import 해야 해요.
import './matchMedia.mock'; // Must be imported before the tested file
import { myMethod } from './file-to-test';
describe('myMethod()', () => {
// Test the method here...
});
더 알아보기
- Mock Functions - mock 함수 활용
- ES6 Class Mocks - 클래스 모킹
- The Jest Object - jest.createMockFromModule - 자동 mock 생성