본문 바로가기
카테고리 없음

Jest — JavaScript 테스팅의 표준

by SuldenLion 2026. 6. 25.
반응형

Jest — JavaScript 테스팅의 표준

프론트엔드 개발 · 테스팅 시리즈 #1
Facebook이 만들고 전 세계 JavaScript 프로젝트가 채택한 테스트 프레임워크. 단위 테스트, 스냅샷 테스트, 모킹 시스템까지 — Jest의 모든 것을 실전 코드와 함께 완전히 마스터합니다.


목차

  1. Jest란 무엇인가
  2. 설치 및 기본 설정
  3. 테스트 구조 — describe, it, expect
  4. Matchers — 단언 메서드 완전 가이드
  5. 비동기 테스트
  6. 모킹(Mocking) 완전 가이드
  7. 스냅샷 테스트
  8. 코드 커버리지
  9. TypeScript 설정
  10. 결론

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 테스팅 생태계의 공용어입니다. 단위 테스트부터 통합 테스트까지, 올인원으로 해결하는 가장 성숙한 선택입니다.


다음 글: Vitest — Vite 네이티브 테스트 프레임워크

반응형

댓글