본문 바로가기
JavaScript/React

Jest와 React Test Library(RTL)

by curious week 2025. 3. 25.

Jest는 테스트 실행기 + 검증 도구

React Testing Library (RTL)는 React 컴포넌트 렌더링 & DOM 쿼리 도구


 

Jest는 언제 사용하는가?

비즈니스 로직 / 유틸 함수 / API / 상태 변경을 테스트할 때

// utils/calc.ts
export const sum = (a: number, b: number) => a + b;
test('sum 함수 테스트', () => {
  expect(sum(2, 3)).toBe(5); // ✅ Jest만으로 충분
});

사용하는 기능

  • test(), describe(), expect()
  • jest.fn(), mockReturnValue()
  • beforeEach(), afterAll()
  • resolves, rejects (비동기 처리)

컴포넌트가 아닌 일반 함수/비동기 로직은 Jest만으로 테스트 가능합니다.


React Testing Library는 언제 사용하는가?

React 컴포넌트의 렌더링, 사용자 상호작용, UI 반응을 테스트할 때

render(<Button label="클릭" onClick={fn} />);
const button = screen.getByText("클릭");
fireEvent.click(button); // ✅ 사용자가 클릭하는 행위를 시뮬레이션

사용하는 기능

  • render() : 컴포넌트를 가상 DOM에 렌더링
  • screen.getByText(), getByRole() 등 쿼리 함수
  • fireEvent, userEvent로 이벤트 발생
  • waitFor, findByText()로 비동기 테스트

컴포넌트를 테스트할 땐 반드시 React Testing Library가 필요
대신 검증(expect)은 Jest를 사용 → 그래서 항상 두 도구를 같이 쓰는 것처럼 보이는 것

함수, 유틸 로직 Jest 단독 sum(), formatDate() 등
API 호출 테스트 (with MSW) Jest + mock 도구 fetchUser() 등
컴포넌트 렌더링 Jest + RTL render(<MyComponent />)
UI에 특정 텍스트가 있는지 Jest + RTL getByText("로그인")
버튼 클릭 이벤트 처리 Jest + RTL fireEvent.click(button)
비동기 렌더링 확인 Jest + RTL + findByText 로딩 후 "완료" 메시지 확인 등
// Jest + RTL 조합
test('버튼 클릭 시 텍스트 변경', () => {
  render(<MyButton />);
  fireEvent.click(screen.getByText('클릭')); // RTL
  expect(screen.getByText('완료')).toBeInTheDocument(); // Jest + RTL
});

 

1. Jest의 역할

Test Runner .test.js 또는 .spec.js 파일을 찾아 실행
Assertion Library expect() 구문으로 결과를 검증
Mocking 기능 의존성을 가짜로 만들어 테스트 가능하게 함

2. 기본 사용 구조

test('설명', () => {
  // 1. 준비 (Arrange)
  // 2. 실행 (Act)
  // 3. 검증 (Assert)
});

예시:

test('1 + 2는 3이다', () => {
  const result = 1 + 2;
  expect(result).toBe(3);
});

Jest 폴더

__폴더명__(예: __tests__, __mocks__, __snapshots__)처럼 언더스코어(__)로 감싼 폴더명은 특별한 의미가 있는 관례적인 구조

__tests__ ❌ 선택 코드와 테스트를 분리하고 싶을 때
__mocks__ 🔶 조건부 Jest의 자동 mock 기능을 쓸 경우
__snapshots__ ✅ 자동 생성 스냅샷 테스트를 사용할 경우에만

→ 반드시 써야 하는 건 아니고, 구조적 정리가 필요할 때 선택적으로 사용


3. 자주 쓰는 Jest 메서드 & 기능

test 또는 it

test('설명', () => {...});
it('설명', () => {...}); // 동일함

expect()

expect(실제값).matcher(예상값) 형태로 작성

.toBe(value) === 값 비교
.toEqual(obj) 객체 값 비교 (deep comparison)
.toContain(item) 배열 또는 문자열 포함 여부
.toHaveLength(n) 길이 검사
.toBeTruthy() / .toBeFalsy() 참/거짓 여부
.toBeNull() / .toBeUndefined() null 또는 undefined 여부
.toBeGreaterThan(n) 숫자 비교
.toMatch(regex) 문자열 정규표현식 비교
.toHaveBeenCalled() mock 함수 호출 여부
.toHaveBeenCalledTimes(n) 몇 번 호출됐는지
.toHaveBeenCalledWith(arg) 어떤 인자로 호출됐는지

4. 구조화 도우미

describe()

  • 관련 테스트들을 그룹화
describe('덧셈 함수', () => {
  test('1 + 1 = 2', () => { ... });
  test('0 + 0 = 0', () => { ... });
});

beforeEach / afterEach

  • 매 테스트 전후에 공통 동작 실행
beforeEach(() => {
  console.log('매 테스트마다 실행됨');
});

beforeAll / afterAll

  • 전체 테스트 한 번만 실행
beforeAll(() => {
  console.log('테스트 시작 전 한 번 실행');
});

5. Mock 관련 메서드

jest.fn()

  • 가짜 함수(mock 함수)를 생성
const mockFn = jest.fn();
mockFn('hello');
expect(mockFn).toHaveBeenCalledWith('hello');

jest.mock()

  • 외부 모듈 전체를 mock 처리
jest.mock('../api/userAPI'); // userAPI 모듈을 자동으로 mock 처리

mockReturnValue()

const mock = jest.fn().mockReturnValue(42);
expect(mock()).toBe(42);

mockResolvedValue() / mockRejectedValue()

  • 비동기 함수의 반환값도 설정 가능
const fetchUser = jest.fn().mockResolvedValue({ name: 'mars112' });
const failAPI = jest.fn().mockRejectedValue(new Error('실패!'));

6. 비동기 테스트

1) async/await 사용

test('비동기 함수가 값을 반환해야 한다', async () => {
  const fetchData = () => Promise.resolve(42);
  const result = await fetchData();
  expect(result).toBe(42);
});

2) .resolves / .rejects

test('resolves matcher 사용', () => {
  return expect(Promise.resolve('OK')).resolves.toBe('OK');
});

test('rejects matcher 사용', () => {
  return expect(Promise.reject('에러')).rejects.toBe('에러');
});

 

유틸 함수 테스트

// utils/math.ts
export const add = (a: number, b: number) => a + b;
// math.test.ts
import { add } from './math';

test('add는 두 수를 더한다', () => {
  expect(add(2, 3)).toBe(5);
});

컴포넌트 테스트 (React Testing Library 사용)

// Button.tsx
export const Button = ({ onClick }: { onClick: () => void }) => (
  <button onClick={onClick}>클릭</button>
);
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

test('버튼 클릭 시 함수가 호출됨', () => {
  const onClick = jest.fn();
  render(<Button onClick={onClick} />);
  fireEvent.click(screen.getByText('클릭'));
  expect(onClick).toHaveBeenCalledTimes(1);
});

8. Jest 설정 파일 (jest.config.ts)

export default {
  testEnvironment: 'jsdom', // 브라우저 환경
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1', // 절대경로 import 지원
  },
};

실무에 바로 쓸 수 있게 정리

1. setupTests.ts 구성

테스트 환경을 초기화하는 전역 설정 파일입니다.
보통 @testing-library/jest-dom 같은 확장 matcher를 불러오거나, 공통 mock을 여기에 정의해요.

파일 위치

src/
├── setupTests.ts   ← 여기에 설정

예시

// src/setupTests.ts
import '@testing-library/jest-dom'; // toBeInTheDocument 등 확장 matcher 지원

jest.config.ts에 등록 필요

// jest.config.ts
export default {
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

2. Mock Server 연동 테스트 (MSW 사용)

**Mock Service Worker (MSW)**를 사용하면 fetch, axios 요청을 가짜 서버로 처리 가능
실제 API가 없어도 테스트 가능하고, 비동기 테스트에서 매우 유용합니다.

설치

npm install msw --save-dev

구조

src/
├── mocks/
│   ├── handlers.ts
│   └── server.ts
├── setupTests.ts

handlers.ts

import { rest } from 'msw';

export const handlers = [
  rest.get('/api/user', (req, res, ctx) => {
    return res(ctx.status(200), ctx.json({ name: 'mars112' }));
  }),
];

server.ts

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

setupTests.ts

import '@testing-library/jest-dom';
import { server } from './mocks/server';

// 테스트 전체 전/후 훅 등록
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

테스트

import { render, screen } from '@testing-library/react';
import { useEffect, useState } from 'react';

const User = () => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);
  return <div>{user ? user.name : '로딩 중...'}</div>;
};

test('유저 데이터를 렌더링한다', async () => {
  render(<User />);
  const name = await screen.findByText('mars112');
  expect(name).toBeInTheDocument();
});

3. Jest로 TDD

TDD(Test-Driven Development): “테스트 먼저 쓰고 → 코드를 작성해서 통과시킴”


이메일 유효성 검사 함수

Step 1: 테스트부터 작성 (email.test.ts)

import { validateEmail } from './email'; // 아직 없음

test('올바른 이메일은 통과해야 한다', () => {
  expect(validateEmail('abc@example.com')).toBe(true);
});

test('잘못된 이메일은 실패해야 한다', () => {
  expect(validateEmail('hello@wrong')).toBe(false);
  expect(validateEmail('no-at-symbol.com')).toBe(false);
});

테스트 실패 (validateEmail이 아직 없음)


Step 2: 함수 생성 & 테스트 통과시키기 (email.ts)

export const validateEmail = (email: string) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

npm test → 통과됨!

jest test code 작성 순서

  1. 실패하는 테스트 작성
  2. 테스트를 통과할 최소한의 코드 작성
  3. 통과 후 리팩토링
  4. 기능 안정성과 의도 확인 가능

 

1. React Testing Library(RTL)의 역할

렌더링 도구 render()로 컴포넌트를 가상 DOM에 마운트
쿼리 함수 실제 사용자처럼 텍스트/레이블 등을 기준으로 DOM 요소 찾기
이벤트 시뮬레이션 fireEvent, userEvent로 클릭/입력/포커스 등 실행
비동기 처리 지원 findBy, waitFor로 비동기 렌더링 대응 가능

2. 기본 사용 구조

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('설명', () => {
  render(<컴포넌트 />);
  const 요소 = screen.getByText('텍스트');
  userEvent.click(요소);
  expect(요소).toBeInTheDocument();
});

3. 자주 쓰는 메서드 & 쿼리 함수

render()

  • 컴포넌트를 테스트 환경에서 가상 DOM에 마운트
render(<MyComponent />);

screen의 쿼리 함수들

getByText() 텍스트로 요소 찾기 (하나만 있을 때)
getByRole() 역할(버튼, heading 등)으로 찾기
getByLabelText() <label>과 연결된 input 찾기
getByPlaceholderText() placeholder로 찾기
getByTestId() data-testid 속성으로 찾기
queryBy... 요소가 없을 수도 있을 때
findBy... 비동기적으로 기다렸다가 찾기 (Promise)
expect(screen.getByText("제목")).toBeInTheDocument();

이벤트 도구

fireEvent

  • 기본 DOM 이벤트 (예: click, change)
fireEvent.click(screen.getByText('클릭'));

userEvent

  • 실제 사용자 행동에 가까운 고급 시뮬레이션
await userEvent.type(input, 'Hello');
await userEvent.click(button);

userEvent는 실제로 input에 포커스 → 타이핑 → blur 순으로 작동합니다.


4. 비동기 처리

test('로딩 후 데이터 표시', async () => {
  render(<UserProfile />);
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();

  const name = await screen.findByText('mars112');
  expect(name).toBeInTheDocument();
});

추가 유틸: waitFor()

await waitFor(() => {
  expect(screen.getByText('완료')).toBeInTheDocument();
});

 

입력폼 테스트

test('입력값이 반영되는가', async () => {
  render(<input placeholder="이름 입력" />);
  const input = screen.getByPlaceholderText('이름 입력');
  await userEvent.type(input, 'mars112');
  expect(input).toHaveValue('mars112');
});

버튼 클릭 이벤트

test('버튼 클릭 시 텍스트 변경', async () => {
  render(<Button />);
  const btn = screen.getByText('시작');
  await userEvent.click(btn);
  expect(screen.getByText('완료')).toBeInTheDocument();
});

6. 테스트용 속성 - data-testid

CSS class, id 같은 스타일 속성이 아니라, 테스트 전용으로 쓰이는 속성

<div data-testid="user-name">mars112</div>
const name = screen.getByTestId('user-name');
expect(name).toHaveTextContent('mars112');

가능하면 getByText, getByRole 등 사용자 중심 접근 우선 사용 → getByTestId는 최후의 수단


7. 전역 설정 (setupTests.ts)

// setupTests.ts
import '@testing-library/jest-dom'; // 확장 matcher 제공 (toBeInTheDocument 등)

등록 위치 (jest.config.ts)

export default {
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
};

8. 확장 matcher (@testing-library/jest-dom)

expect(element).toBeInTheDocument();
expect(button).toBeDisabled();
expect(input).toHaveValue('mars112');
expect(link).toHaveAttribute('href', '/home');

이런 matcher는 jest-dom을 통해 사용!


 

1. 비동기 컴포넌트 테스트 실전 예제

컴포넌트: User.tsx

import { useEffect, useState } from 'react';

export const User = () => {
  const [name, setName] = useState('');
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => {
        setName(data.name);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>로딩 중...</p>;

  return <h1>안녕하세요, {name}님</h1>;
};

테스트 코드: User.test.tsx (with MSW)

import { render, screen } from '@testing-library/react';
import { User } from './User';
import { rest } from 'msw';
import { setupServer } from 'msw/node';

// Mock server
const server = setupServer(
  rest.get('/api/user', (req, res, ctx) =>
    res(ctx.status(200), ctx.json({ name: 'mars112' }))
  )
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('사용자 데이터를 로딩 후 표시해야 함', async () => {
  render(<User />);
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();

  const name = await screen.findByText('안녕하세요, mars112님');
  expect(name).toBeInTheDocument();
});

2. Form Validation 테스트

컴포넌트: LoginForm.tsx

import { useState } from 'react';

export const LoginForm = () => {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!email.includes('@')) {
      setError('올바른 이메일을 입력하세요');
    } else {
      setError('');
      alert('로그인 성공');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        placeholder="이메일"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">로그인</button>
      {error && <p>{error}</p>}
    </form>
  );
};

테스트 코드: LoginForm.test.tsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

test('올바르지 않은 이메일 입력 시 오류 메시지 출력', async () => {
  render(<LoginForm />);
  const input = screen.getByPlaceholderText('이메일');
  const button = screen.getByText('로그인');

  await userEvent.type(input, 'wrong-email');
  await userEvent.click(button);

  expect(screen.getByText('올바른 이메일을 입력하세요')).toBeInTheDocument();
});

3. Mocking Axios 요청 테스트

컴포넌트: PostList.tsx

import { useEffect, useState } from 'react';
import axios from 'axios';

export const PostList = () => {
  const [posts, setPosts] = useState<string[]>([]);

  useEffect(() => {
    axios.get('/api/posts').then((res) => setPosts(res.data));
  }, []);

  return (
    <ul>
      {posts.map((title, i) => (
        <li key={i}>{title}</li>
      ))}
    </ul>
  );
};

테스트 코드: PostList.test.tsx

import { render, screen } from '@testing-library/react';
import { PostList } from './PostList';
import axios from 'axios';

jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

test('axios를 통해 글 목록을 가져와 렌더링', async () => {
  mockedAxios.get.mockResolvedValue({ data: ['글1', '글2'] });

  render(<PostList />);
  const items = await screen.findAllByRole('listitem');
  expect(items).toHaveLength(2);
  expect(items[0]).toHaveTextContent('글1');
});