목록으로
React

React 테스트 작성 가이드

Jest와 React Testing Library로 실용적인 테스트를 작성하는 방법을 알아봅니다.

테스트는 코드의 안정성을 보장합니다. 하지만 테스트를 어떻게 작성해야 할지 막막할 때가 많습니다. 실용적인 React 테스트 작성법을 알아봅니다.

테스트 환경 설정

Create React App이나 Next.js를 사용한다면 Jest가 기본으로 설정되어 있습니다. React Testing Library를 추가로 설치합니다.

npm install --save-dev @testing-library/react @testing-library/jest-dom

기본 테스트 구조

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

describe('Button', () => {
  it('텍스트를 표시한다', () => {
    render(<Button>클릭</Button>);
    expect(screen.getByText('클릭')).toBeInTheDocument();
  });

  it('클릭하면 onClick이 호출된다', async () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>클릭</Button>);

    await userEvent.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('disabled 상태에서는 클릭이 무시된다', async () => {
    const handleClick = jest.fn();
    render(<Button disabled onClick={handleClick}>클릭</Button>);

    await userEvent.click(screen.getByRole('button'));

    expect(handleClick).not.toHaveBeenCalled();
  });
});

쿼리 선택하기

React Testing Library는 사용자 관점의 쿼리를 권장합니다.

우선순위 (높은 순)

  1. getByRole - 접근성 역할로 찾기
  2. getByLabelText - 라벨 텍스트로 찾기
  3. getByPlaceholderText - placeholder로 찾기
  4. getByText - 텍스트로 찾기
  5. getByTestId - data-testid로 찾기 (최후의 수단)
// 좋은 예 - 사용자가 보는 것으로 찾기
screen.getByRole('button', { name: '제출' });
screen.getByLabelText('이메일');
screen.getByText('로그인');

// 피해야 할 예 - 구현 세부사항에 의존
screen.getByTestId('submit-button');
container.querySelector('.btn-primary');

비동기 테스트

API 호출 등 비동기 작업은 waitFor나 findBy를 사용합니다.

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

it('데이터를 로드하고 표시한다', async () => {
  render(<UserList />);

  // 로딩 상태 확인
  expect(screen.getByText('로딩 중...')).toBeInTheDocument();

  // 데이터 로드 대기
  await waitFor(() => {
    expect(screen.getByText('홍길동')).toBeInTheDocument();
  });

  // 또는 findBy 사용 (waitFor + getBy)
  const user = await screen.findByText('홍길동');
  expect(user).toBeInTheDocument();
});

API 모킹

MSW(Mock Service Worker)를 사용하면 네트워크 레벨에서 API를 모킹할 수 있습니다.

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.json([
        { id: 1, name: '홍길동' },
        { id: 2, name: '김영희' },
      ])
    );
  })
);

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

it('사용자 목록을 표시한다', async () => {
  render(<UserList />);

  await screen.findByText('홍길동');
  expect(screen.getByText('김영희')).toBeInTheDocument();
});

it('에러 시 에러 메시지를 표시한다', async () => {
  server.use(
    rest.get('/api/users', (req, res, ctx) => {
      return res(ctx.status(500));
    })
  );

  render(<UserList />);

  await screen.findByText('오류가 발생했습니다');
});

커스텀 훅 테스트

renderHook을 사용합니다.

import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
  it('초기값을 설정할 수 있다', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('increment가 값을 증가시킨다', () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });
});

무엇을 테스트할까

테스트해야 할 것

  • 사용자 상호작용 (클릭, 입력 등)
  • 조건부 렌더링
  • 폼 제출과 유효성 검사
  • 에러 상태 처리
  • 비즈니스 로직

테스트하지 않아도 될 것

  • 라이브러리 코드 (이미 테스트됨)
  • 스타일링
  • 구현 세부사항

테스트 작성 팁

하나의 테스트에 하나의 검증

// 나쁜 예 - 여러 가지를 한 번에 테스트
it('폼이 제대로 동작한다', async () => {
  render(<LoginForm />);
  // 입력, 제출, 성공, 에러 모두 한 테스트에...
});

// 좋은 예 - 각각 분리
it('이메일 입력이 가능하다', () => { ... });
it('비밀번호 입력이 가능하다', () => { ... });
it('유효한 입력 시 제출된다', () => { ... });
it('잘못된 이메일 시 에러를 표시한다', () => { ... });

테스트 이름은 명확하게

테스트 이름만 보고도 무엇을 테스트하는지 알 수 있어야 합니다.

// 나쁜 예
it('동작한다', () => { ... });
it('테스트 1', () => { ... });

// 좋은 예
it('로그인 버튼 클릭 시 API를 호출한다', () => { ... });
it('잘못된 비밀번호 입력 시 에러 메시지를 표시한다', () => { ... });

마무리

테스트는 사용자 관점에서 작성하는 것이 중요합니다. 구현 세부사항보다는 컴포넌트가 사용자에게 어떻게 동작하는지에 집중합니다.

100% 커버리지를 목표로 하기보다는, 중요한 기능과 엣지 케이스를 먼저 테스트하는 것이 실용적입니다.