한국어

이 완전한 가이드를 통해 React Testing Library(RTL)를 마스터하세요. 모범 사례와 실제 예제에 초점을 맞춰 React 애플리케이션을 위한 효과적이고 유지보수 가능하며 사용자 중심적인 테스트를 작성하는 방법을 배워보세요.

React Testing Library: 종합 가이드

오늘날 급변하는 웹 개발 환경에서 React 애플리케이션의 품질과 안정성을 보장하는 것은 매우 중요합니다. React Testing Library(RTL)는 사용자 관점에 초점을 맞춘 테스트를 작성하기 위한 인기 있고 효과적인 솔루션으로 부상했습니다. 이 가이드는 RTL에 대한 완전한 개요를 제공하며, 기본 개념부터 고급 기술까지 모든 것을 다루어 견고하고 유지보수 가능한 React 애플리케이션을 구축할 수 있도록 지원합니다.

React Testing Library를 선택해야 하는 이유

기존의 테스트 접근 방식은 종종 구현 세부 정보에 의존하여 테스트를 불안정하게 만들고 사소한 코드 변경에도 깨지기 쉽습니다. 반면 RTL은 사용자가 컴포넌트와 상호 작용하는 것처럼 테스트하여 사용자가 보고 경험하는 것에 초점을 맞추도록 권장합니다. 이 접근 방식은 다음과 같은 몇 가지 주요 이점을 제공합니다:

테스트 환경 설정하기

RTL 사용을 시작하기 전에 테스트 환경을 설정해야 합니다. 여기에는 일반적으로 필요한 종속성 설치 및 테스트 프레임워크 구성이 포함됩니다.

사전 요구사항

설치

npm 또는 yarn을 사용하여 다음 패키지를 설치하세요:

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

또는 yarn 사용 시:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

패키지 설명:

구성

프로젝트 루트에 다음 내용으로 `babel.config.js` 파일을 만드세요:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

`package.json` 파일을 업데이트하여 테스트 스크립트를 포함시키세요:

{
  "scripts": {
    "test": "jest"
  }
}

프로젝트 루트에 Jest를 구성하기 위한 `jest.config.js` 파일을 만드세요. 최소한의 구성은 다음과 같을 수 있습니다:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

다음 내용으로 `src/setupTests.js` 파일을 만드세요. 이렇게 하면 모든 테스트에서 Jest DOM 매처를 사용할 수 있습니다:

import '@testing-library/jest-dom/extend-expect';

첫 테스트 작성하기

간단한 예제로 시작하겠습니다. 인사말 메시지를 표시하는 React 컴포넌트가 있다고 가정해 보겠습니다:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

export default Greeting;

이제 이 컴포넌트에 대한 테스트를 작성해 보겠습니다:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('renders a greeting message', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/Hello, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

설명:

테스트를 실행하려면 터미널에서 다음 명령을 실행하세요:

npm test

모든 것이 올바르게 구성되었다면 테스트가 통과될 것입니다.

일반적인 RTL 쿼리

RTL은 DOM에서 요소를 찾기 위한 다양한 쿼리 메서드를 제공합니다. 이 쿼리들은 사용자가 애플리케이션과 상호 작용하는 방식을 모방하도록 설계되었습니다.

`getByRole`

이 쿼리는 ARIA 역할로 요소를 찾습니다. 접근성을 높이고 테스트가 기본 DOM 구조의 변경에 탄력적으로 대응하도록 하기 위해 가능하면 `getByRole`을 사용하는 것이 좋습니다.

<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

이 쿼리는 연관된 레이블의 텍스트로 요소를 찾습니다. 폼 요소를 테스트하는 데 유용합니다.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

이 쿼리는 플레이스홀더 텍스트로 요소를 찾습니다.

<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

이 쿼리는 이미지 요소의 alt 텍스트로 찾습니다. 접근성을 보장하기 위해 모든 이미지에 의미 있는 alt 텍스트를 제공하는 것이 중요합니다.

<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

이 쿼리는 title 속성으로 요소를 찾습니다.

<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

이 쿼리는 표시 값으로 요소를 찾습니다. 미리 채워진 값을 가진 폼 입력을 테스트하는 데 유용합니다.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

`getAllBy*` 쿼리

`getBy*` 쿼리 외에도 RTL은 일치하는 요소의 배열을 반환하는 `getAllBy*` 쿼리도 제공합니다. 이는 동일한 특성을 가진 여러 요소가 DOM에 존재하는지 단언해야 할 때 유용합니다.

<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

`queryBy*` 쿼리

`queryBy*` 쿼리는 `getBy*` 쿼리와 유사하지만, 일치하는 요소를 찾지 못하면 오류를 던지는 대신 `null`을 반환합니다. 이는 요소가 DOM에 *존재하지 않음*을 단언하고 싶을 때 유용합니다.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

`findBy*` 쿼리

`findBy*` 쿼리는 `getBy*` 쿼리의 비동기 버전입니다. 일치하는 요소를 찾았을 때 해결되는 Promise를 반환합니다. 이는 API에서 데이터를 가져오는 것과 같은 비동기 작업을 테스트하는 데 유용합니다.

// Simulating an asynchronous data fetch
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Data Loaded!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Data Loaded!');
  expect(dataElement).toBeInTheDocument();
});

사용자 상호작용 시뮬레이션

RTL은 버튼 클릭, 입력 필드에 타이핑, 폼 제출과 같은 사용자 상호작용을 시뮬레이션하기 위한 `fireEvent`와 `userEvent` API를 제공합니다.

`fireEvent`

`fireEvent`를 사용하면 프로그래밍 방식으로 DOM 이벤트를 트리거할 수 있습니다. 이는 발생하는 이벤트를 세밀하게 제어할 수 있는 저수준 API입니다.

<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';

test('simulates a button click', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent`는 사용자 상호작용을 더 현실적으로 시뮬레이션하는 고수준 API입니다. 포커스 관리 및 이벤트 순서와 같은 세부 사항을 처리하여 테스트를 더 견고하고 덜 취약하게 만듭니다.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simulates typing in an input field', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Hello, world!');
  expect(inputElement).toHaveValue('Hello, world!');
});

비동기 코드 테스트하기

많은 React 애플리케이션에는 API에서 데이터를 가져오는 것과 같은 비동기 작업이 포함됩니다. RTL은 비동기 코드를 테스트하기 위한 여러 도구를 제공합니다.

`waitFor`

`waitFor`를 사용하면 단언을 하기 전에 조건이 참이 될 때까지 기다릴 수 있습니다. 완료하는 데 시간이 걸리는 비동기 작업을 테스트하는 데 유용합니다.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Data loaded!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('waits for data to load', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Data loaded!'));
  const dataElement = screen.getByText('Data loaded!');
  expect(dataElement).toBeInTheDocument();
});

`findBy*` 쿼리

앞서 언급했듯이 `findBy*` 쿼리는 비동기적이며 일치하는 요소를 찾았을 때 해결되는 Promise를 반환합니다. 이는 DOM의 변경을 초래하는 비동기 작업을 테스트하는 데 유용합니다.

Hook 테스트하기

React Hook은 상태 저장 로직을 캡슐화하는 재사용 가능한 함수입니다. RTL은 `@testing-library/react-hooks`의 `renderHook` 유틸리티(v17부터는 `@testing-library/react`에 직접 포함되어 사용이 중단됨)를 제공하여 사용자 정의 Hook을 격리하여 테스트할 수 있습니다.

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('increments the counter', () => {
  const { result } = renderHook(() => useCounter());

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

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

설명:

고급 테스트 기법

RTL의 기본을 마스터했다면, 테스트의 품질과 유지보수성을 향상시키기 위해 더 고급 테스트 기법을 탐색할 수 있습니다.

모듈 모킹(Mocking)

때로는 외부 모듈이나 종속성을 모의(mock)하여 컴포넌트를 격리하고 테스트 중 동작을 제어해야 할 수 있습니다. Jest는 이를 위한 강력한 모킹 API를 제공합니다.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('fetches data from the API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Mocked data!'));

  expect(screen.getByText('Mocked data!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

설명:

Context Provider

컴포넌트가 Context Provider에 의존하는 경우, 테스트 중에 컴포넌트를 Provider로 감싸야 합니다. 이렇게 하면 컴포넌트가 컨텍스트 값에 접근할 수 있습니다.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('toggles the theme', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Current theme: light/i);
  const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});

설명:

Router로 테스트하기

React Router를 사용하는 컴포넌트를 테스트할 때는 모의 Router 컨텍스트를 제공해야 합니다. 이는 `react-router-dom`의 `MemoryRouter` 컴포넌트를 사용하여 달성할 수 있습니다.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Go to About Page</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('renders a link to the about page', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

설명:

효과적인 테스트 작성을 위한 모범 사례

RTL로 테스트를 작성할 때 따라야 할 몇 가지 모범 사례는 다음과 같습니다:

결론

React Testing Library는 React 애플리케이션을 위한 효과적이고 유지보수 가능하며 사용자 중심적인 테스트를 작성하기 위한 강력한 도구입니다. 이 가이드에서 설명한 원칙과 기법을 따르면 사용자의 요구를 충족하는 견고하고 신뢰할 수 있는 애플리케이션을 구축할 수 있습니다. 사용자 관점에서 테스트하는 데 집중하고, 구현 세부 정보를 테스트하는 것을 피하며, 명확하고 간결한 테스트를 작성하는 것을 기억하세요. RTL을 도입하고 모범 사례를 채택함으로써, 현재 위치나 글로벌 고객의 특정 요구사항에 관계없이 React 프로젝트의 품질과 유지보수성을 크게 향상시킬 수 있습니다.