Jest 완벽 가이드 | 유닛 테스트·Mock·Snapshot·Coverage·React Testing
이 글의 핵심
Jest로 JavaScript 테스트를 작성하는 완벽 가이드입니다. 유닛 테스트, Mock, Snapshot, Coverage, React Testing Library 통합까지 실전 예제로 정리했습니다.
실무 경험 공유: 테스트가 없던 프로젝트에 Jest를 도입하면서, 버그 발견율을 3배 높이고 리팩토링 자신감을 크게 향상시킨 경험을 공유합니다.
들어가며: “테스트가 없어요”
실무 문제 시나리오
시나리오 1: 리팩토링이 무서워요
코드를 수정하면 뭐가 깨질지 모릅니다. Jest로 안전하게 리팩토링합니다.
시나리오 2: 버그를 늦게 발견해요
프로덕션에서 버그를 발견합니다. Jest로 개발 중 발견합니다.
시나리오 3: 문서가 없어요
함수 사용법을 모릅니다. 테스트가 문서 역할을 합니다.
1. Jest란?
핵심 특징
Jest는 JavaScript 테스팅 프레임워크입니다.
주요 장점:
- Zero Config: 설정 없이 시작
- 빠른 실행: 병렬 실행
- Snapshot: UI 변경 감지
- Mock: 의존성 모킹
- Coverage: 코드 커버리지
2. 설치
npm install -D jest @types/jest
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.test.{js,ts}',
],
};
3. 기본 테스트
단순 함수
// src/utils/math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// src/utils/math.test.ts
import { add, multiply } from './math';
describe('Math utils', () => {
test('add should sum two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
});
test('multiply should multiply two numbers', () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(0, 5)).toBe(0);
});
});
4. Matchers
// 동등성
expect(value).toBe(5);
expect(value).toEqual({ name: 'John' });
// 참/거짓
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
// 숫자
expect(value).toBeGreaterThan(3);
expect(value).toBeLessThan(10);
expect(value).toBeCloseTo(0.3);
// 문자열
expect(value).toMatch(/hello/i);
expect(value).toContain('world');
// 배열
expect(array).toContain('item');
expect(array).toHaveLength(3);
// 객체
expect(obj).toHaveProperty('name');
expect(obj).toMatchObject({ name: 'John' });
// 에러
expect(() => throwError()).toThrow();
expect(() => throwError()).toThrow('Error message');
5. 비동기 테스트
async/await
async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
test('fetchUser should return user', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('name');
expect(user.id).toBe(1);
});
Promise
test('fetchUser should return user', () => {
return fetchUser(1).then((user) => {
expect(user).toHaveProperty('name');
});
});
6. Mock
함수 Mock
const mockCallback = jest.fn((x) => x * 2);
[1, 2, 3].forEach(mockCallback);
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith(1);
expect(mockCallback).toHaveBeenLastCalledWith(3);
expect(mockCallback.mock.results[0].value).toBe(2);
모듈 Mock
// src/api.ts
export async function fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}
// src/api.test.ts
import { fetchUsers } from './api';
jest.mock('./api');
test('should fetch users', async () => {
(fetchUsers as jest.Mock).mockResolvedValue([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
const users = await fetchUsers();
expect(users).toHaveLength(2);
});
7. React Testing Library
설치
npm install -D @testing-library/react @testing-library/jest-dom
컴포넌트 테스트
// src/components/Counter.tsx
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
// src/components/Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Counter } from './Counter';
describe('Counter', () => {
test('should render initial count', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
test('should increment count', () => {
render(<Counter />);
const incrementButton = screen.getByText('Increment');
fireEvent.click(incrementButton);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
test('should decrement count', () => {
render(<Counter />);
const decrementButton = screen.getByText('Decrement');
fireEvent.click(decrementButton);
expect(screen.getByText('Count: -1')).toBeInTheDocument();
});
});
8. Snapshot Testing
// src/components/UserCard.tsx
interface UserCardProps {
name: string;
email: string;
}
export function UserCard({ name, email }: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
// src/components/UserCard.test.tsx
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';
test('should match snapshot', () => {
const { container } = render(
<UserCard name="John" email="[email protected]" />
);
expect(container).toMatchSnapshot();
});
9. Coverage
# Coverage 실행
npm run test:coverage
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{js,ts,tsx}',
'!src/**/*.test.{js,ts,tsx}',
'!src/index.tsx',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
정리 및 체크리스트
핵심 요약
- Jest: JavaScript 테스팅 프레임워크
- 유닛 테스트: 함수, 모듈 테스트
- Mock: 의존성 모킹
- Snapshot: UI 변경 감지
- Coverage: 코드 커버리지
- React Testing Library: 컴포넌트 테스트
구현 체크리스트
- Jest 설치
- 첫 테스트 작성
- Mock 활용
- React 컴포넌트 테스트
- Snapshot 테스트
- Coverage 80% 이상
- CI/CD 통합
같이 보면 좋은 글
- Vitest 완벽 가이드
- React 18 심화 가이드
- Cypress E2E 테스팅 가이드
이 글에서 다루는 키워드
Jest, Testing, Unit Test, Mock, React, JavaScript, TypeScript
자주 묻는 질문 (FAQ)
Q. Jest vs Vitest, 어떤 게 나은가요?
A. Vitest가 더 빠릅니다. Vite 프로젝트는 Vitest, 그 외는 Jest를 권장합니다.
Q. 테스트 커버리지 목표는?
A. 80% 이상을 권장합니다. 하지만 100%를 목표로 하지 마세요. 중요한 로직에 집중하세요.
Q. E2E 테스트도 Jest로 하나요?
A. 아니요, E2E는 Cypress나 Playwright를 사용하세요.
Q. Snapshot 테스트는 언제 사용하나요?
A. UI 컴포넌트의 의도하지 않은 변경을 감지할 때 유용합니다. 하지만 남용하지 마세요.