Jest 완벽 가이드 | 유닛 테스트·Mock·Snapshot·Coverage·React Testing

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 컴포넌트의 의도하지 않은 변경을 감지할 때 유용합니다. 하지만 남용하지 마세요.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3