반응형
Jest — JavaScript 테스팅의 표준
프론트엔드 개발 · 테스팅 시리즈 #1
Facebook이 만들고 전 세계 JavaScript 프로젝트가 채택한 테스트 프레임워크. 단위 테스트, 스냅샷 테스트, 모킹 시스템까지 — Jest의 모든 것을 실전 코드와 함께 완전히 마스터합니다.
목차
- Jest란 무엇인가
- 설치 및 기본 설정
- 테스트 구조 — describe, it, expect
- Matchers — 단언 메서드 완전 가이드
- 비동기 테스트
- 모킹(Mocking) 완전 가이드
- 스냅샷 테스트
- 코드 커버리지
- TypeScript 설정
- 결론
1. Jest란 무엇인가
Jest는 2014년 Facebook이 만든 JavaScript 테스트 프레임워크입니다. 테스트 러너, 단언 라이브러리, 모킹 시스템, 코드 커버리지를 하나의 패키지로 제공하는 올인원 솔루션입니다.
Jest = 테스트 러너 (테스트 파일 발견·실행)
+ 단언 라이브러리 (expect, matchers)
+ 모킹 시스템 (jest.fn, jest.mock, jest.spyOn)
+ 스냅샷 테스트
+ 코드 커버리지 (Istanbul 내장)
+ 병렬 실행 (Worker 기반)
+ Watch 모드Jest가 지배적인 이유
- 제로 설정(Zero Config):
npm install jest후 바로 사용 가능 - 빠른 실행: 테스트 파일을 병렬로 실행
- 격리된 환경: 각 테스트 파일이 독립 VM에서 실행
- 풍부한 생태계: React Testing Library, jest-dom 등과 자연스럽게 통합
2. 설치 및 기본 설정
설치
npm install --save-dev jest
# TypeScript 사용 시
npm install --save-dev jest @types/jest ts-jest
# ESM 환경
npm install --save-dev jest babel-jest @babel/core @babel/preset-env
jest.config.js
// jest.config.js
/** @type {import('jest').Config} */
module.exports = {
// 테스트 환경 (기본: node, 브라우저 DOM 필요 시: jsdom)
testEnvironment: 'node',
// 테스트 파일 패턴
testMatch: [
'**/__tests__/**/*.{js,ts}',
'**/*.{spec,test}.{js,ts}',
],
// 특정 파일/폴더 제외
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
// 모듈 별칭 (tsconfig paths와 동기화)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|scss|less)$': '<rootDir>/__mocks__/styleMock.js',
'\\.(png|jpg|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
// 각 테스트 파일 실행 전 설정 파일
setupFilesAfterFramework: ['<rootDir>/src/setupTests.ts'],
// 커버리지 설정
collectCoverageFrom: [
'src/**/*.{js,ts,jsx,tsx}',
'!src/**/*.d.ts',
'!src/index.ts',
],
coverageThresholds: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
// 변환 설정
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
};
package.json 스크립트
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --runInBand"
}
}
3. 테스트 구조 — describe, it, expect
// math.test.js
// describe: 관련 테스트를 그룹화
describe('add 함수', () => {
// it (또는 test): 개별 테스트 케이스
it('두 양수를 더한다', () => {
// expect + matcher로 단언
expect(add(2, 3)).toBe(5);
});
it('음수를 더한다', () => {
expect(add(-1, -2)).toBe(-3);
});
it('0을 더하면 값이 변하지 않는다', () => {
expect(add(5, 0)).toBe(5);
});
});
describe('divide 함수', () => {
it('정상 나눗셈을 수행한다', () => {
expect(divide(10, 2)).toBe(5);
});
it('0으로 나누면 에러를 던진다', () => {
expect(() => divide(10, 0)).toThrow('0으로 나눌 수 없습니다');
});
});
라이프사이클 훅
describe('UserService', () => {
let db;
// 이 describe 블록의 모든 테스트 전에 한 번 실행
beforeAll(async () => {
db = await connectDatabase();
});
// 이 describe 블록의 모든 테스트 후에 한 번 실행
afterAll(async () => {
await db.disconnect();
});
// 각 테스트 전에 실행 (상태 초기화)
beforeEach(() => {
db.clear();
});
// 각 테스트 후에 실행 (정리)
afterEach(() => {
jest.clearAllMocks();
});
it('사용자를 생성한다', async () => {
const user = await UserService.create({ name: '홍길동' });
expect(user.id).toBeDefined();
});
});
4. Matchers — 단언 메서드 완전 가이드
동등성 검사
// toBe: 원시값 동등성 (Object.is 사용)
expect(1 + 1).toBe(2);
expect('hello').toBe('hello');
// toEqual: 객체/배열의 깊은 동등성 (구조 비교)
expect({ a: 1, b: { c: 2 } }).toEqual({ a: 1, b: { c: 2 } });
expect([1, 2, 3]).toEqual([1, 2, 3]);
// toStrictEqual: toEqual + undefined 프로퍼티 엄격 비교
expect({ a: 1, b: undefined }).not.toStrictEqual({ a: 1 });
진리값 검사
expect(true).toBeTruthy();
expect(1).toBeTruthy();
expect('hello').toBeTruthy();
expect(false).toBeFalsy();
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(42).toBeDefined();
숫자 비교
expect(5).toBeGreaterThan(3);
expect(5).toBeGreaterThanOrEqual(5);
expect(3).toBeLessThan(5);
expect(3).toBeLessThanOrEqual(3);
// 부동소수점 비교 (0.1 + 0.2 문제)
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 소수점 5자리까지
문자열 검사
expect('Hello, World!').toContain('World');
expect('Hello, World!').toMatch(/Hello/);
expect('Hello, World!').toMatch('Hello');
expect('Hello').toHaveLength(5);
배열과 이터러블
const arr = [1, 2, 3, 4, 5];
expect(arr).toContain(3);
expect(arr).toHaveLength(5);
expect(arr).toEqual(expect.arrayContaining([1, 3])); // 순서 무관, 부분 포함
객체 검사
const user = { id: 1, name: '홍길동', role: 'admin' };
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('name', '홍길동');
expect(user).toHaveProperty('id', 1);
// 부분 객체 검사 (추가 프로퍼티 허용)
expect(user).toMatchObject({ name: '홍길동', role: 'admin' });
에러 검사
function riskyFunction() {
throw new TypeError('잘못된 타입');
}
expect(() => riskyFunction()).toThrow();
expect(() => riskyFunction()).toThrow('잘못된 타입');
expect(() => riskyFunction()).toThrow(TypeError);
expect(() => riskyFunction()).toThrow(/잘못된/);
not으로 부정
expect(1).not.toBe(2);
expect(null).not.toBeDefined();
expect([1, 2]).not.toContain(3);
5. 비동기 테스트
Promise / async-await
// async/await 방식 (권장)
it('비동기로 사용자를 가져온다', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('홍길동');
});
// Promise 반환 방식
it('Promise를 반환한다', () => {
return fetchUser(1).then(user => {
expect(user.name).toBe('홍길동');
});
});
// 에러 케이스
it('존재하지 않는 사용자는 에러를 던진다', async () => {
await expect(fetchUser(999)).rejects.toThrow('사용자를 찾을 수 없습니다');
});
콜백 기반 비동기
// done 콜백 — 콜백 기반 API 테스트
it('콜백을 호출한다', (done) => {
fetchUserCallback(1, (error, user) => {
if (error) {
done(error); // 에러 전달
return;
}
expect(user.name).toBe('홍길동');
done(); // 테스트 완료 신호
});
});
타이머 제어
// 가짜 타이머 사용
jest.useFakeTimers();
it('debounce 함수가 지연 후 실행된다', () => {
const callback = jest.fn();
const debounced = debounce(callback, 500);
debounced();
debounced();
debounced();
expect(callback).not.toHaveBeenCalled(); // 아직 실행 안 됨
jest.advanceTimersByTime(500); // 500ms 경과
expect(callback).toHaveBeenCalledTimes(1); // 한 번만 실행
});
afterEach(() => {
jest.useRealTimers(); // 실제 타이머로 복원
});
6. 모킹(Mocking) 완전 가이드
jest.fn() — 가짜 함수
// 가짜 함수 생성
const mockFn = jest.fn();
mockFn(1, 2);
mockFn('hello');
// 호출 여부 확인
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(1, 2);
expect(mockFn).toHaveBeenLastCalledWith('hello');
// 반환값 설정
const mockFn2 = jest.fn().mockReturnValue(42);
expect(mockFn2()).toBe(42);
// 한 번만 특정 값 반환
const mockFn3 = jest.fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
expect(mockFn3()).toBe('first');
expect(mockFn3()).toBe('second');
expect(mockFn3()).toBe('default');
expect(mockFn3()).toBe('default');
// 비동기 반환
const mockAsync = jest.fn().mockResolvedValue({ id: 1, name: '홍길동' });
const user = await mockAsync();
expect(user.name).toBe('홍길동');
// 에러 반환
const mockError = jest.fn().mockRejectedValue(new Error('실패'));
await expect(mockError()).rejects.toThrow('실패');
// 구현 제공
const mockImpl = jest.fn().mockImplementation((a, b) => a * b);
expect(mockImpl(3, 4)).toBe(12);
jest.mock() — 모듈 모킹
// api.js
export async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// userService.test.js
jest.mock('./api'); // api 모듈 전체를 모킹
import { fetchUser } from './api';
import { getUserProfile } from './userService';
it('사용자 프로필을 가져온다', async () => {
// fetchUser를 가짜 구현으로 대체
fetchUser.mockResolvedValue({ id: 1, name: '홍길동', email: 'hong@example.com' });
const profile = await getUserProfile(1);
expect(profile.displayName).toBe('홍길동');
expect(fetchUser).toHaveBeenCalledWith(1);
});
// 팩토리 함수로 세밀한 모킹
jest.mock('./emailService', () => ({
sendEmail: jest.fn().mockResolvedValue({ success: true }),
validateEmail: jest.fn().mockReturnValue(true),
}));
jest.spyOn() — 스파이
// 실제 구현을 유지하면서 호출을 관찰
const consoleSpy = jest.spyOn(console, 'log');
doSomeThing(); // 내부적으로 console.log 호출
expect(consoleSpy).toHaveBeenCalledWith('예상 메시지');
consoleSpy.mockRestore(); // 원래 구현 복원
// 특정 호출만 모킹
const dateSpy = jest.spyOn(Date, 'now').mockReturnValue(1700000000000);
expect(Date.now()).toBe(1700000000000);
dateSpy.mockRestore();
모킹 초기화
beforeEach(() => {
jest.clearAllMocks(); // 모든 mock 호출 기록 초기화 (구현 유지)
});
afterAll(() => {
jest.restoreAllMocks(); // spyOn으로 교체된 것들 복원
jest.resetAllMocks(); // 구현 + 기록 모두 초기화
});
7. 스냅샷 테스트
스냅샷 테스트는 컴포넌트의 렌더링 결과나 데이터 구조를 파일로 저장하고, 이후 변경을 감지합니다.
// utils.test.js
it('사용자 객체 스냅샷', () => {
const user = createUser({ name: '홍길동' });
expect(user).toMatchSnapshot();
// 처음 실행: __snapshots__/utils.test.js.snap 파일 생성
// 이후 실행: 저장된 스냅샷과 비교
});
// 인라인 스냅샷 (파일 없이 코드 내에 저장)
it('에러 메시지 형식', () => {
const error = formatError({ code: 404, message: 'Not Found' });
expect(error).toMatchInlineSnapshot(`
{
"code": 404,
"displayMessage": "[404] Not Found",
"timestamp": Any<Number>,
}
`);
});
스냅샷 업데이트
# 스냅샷이 의도적으로 변경됐을 때 업데이트
jest --updateSnapshot
jest -u
8. 코드 커버리지
# 커버리지 리포트 생성
jest --coverage
# 특정 파일만
jest --coverage --collectCoverageFrom="src/utils/**"
커버리지 리포트 종류:
Statements: 실행된 구문의 비율
Branches: if/else, 삼항연산자 등 분기의 비율
Functions: 호출된 함수의 비율
Lines: 실행된 코드 줄의 비율PASS src/utils/math.test.js
----------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
math.js | 92.31 | 83.33 | 100 | 92.31 |
string.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|
All files | 95.83 | 90.91 | 100 | 95.83 |9. TypeScript 설정
npm install --save-dev ts-jest @types/jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: './tsconfig.test.json',
},
},
};
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["jest", "node"]
}
}
// typed.test.ts
interface User {
id: number;
name: string;
}
const mockFetchUser = jest.fn<Promise<User>, [number]>();
mockFetchUser.mockResolvedValue({ id: 1, name: '홍길동' });
it('타입 안전한 모킹', async () => {
const user = await mockFetchUser(1);
expect(user.name).toBe('홍길동');
});
10. 결론
좋은 테스트의 원칙 (F.I.R.S.T)
Fast — 빠르게 실행 (의존성 모킹으로 외부 통신 제거)
Isolated — 독립적 (다른 테스트에 영향 주지 않음)
Repeatable — 반복 가능 (같은 결과 보장)
Self-Validating — 자체 검증 (expect로 명확한 합격/불합격)
Timely — 적시에 (코드 작성과 함께 또는 직전)Jest 설정 체크리스트
-
jest.config.js작성 및 환경 설정 - TypeScript 사용 시
ts-jest설정 - CSS/파일 import 모킹 설정
- 경로 별칭(
@/) 매핑 -
setupFilesAfterFramework에 전역 설정 - 커버리지 임계값 설정
- CI에서
--ci --runInBand플래그 사용
한 줄 요약: Jest는 JavaScript 테스팅 생태계의 공용어입니다. 단위 테스트부터 통합 테스트까지, 올인원으로 해결하는 가장 성숙한 선택입니다.
반응형
댓글