React Testing Library 철학
React Testing Library는 "사용자가 컴포넌트를 사용하는 방식"으로 테스트합니다. 구현 세부사항이 아닌 동작을 테스트하여 리팩토링에 강한 테스트를 작성합니다.
기본 설정
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts'
}
});
// src/test/setup.ts
import '@testing-library/jest-dom';
기본 렌더링과 쿼리
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('renders login form with email and password fields', () => {
render(<LoginForm onSubmit={vi.fn()} />);
expect(screen.getByLabelText('이메일')).toBeInTheDocument();
expect(screen.getByLabelText('비밀번호')).toBeInTheDocument();
expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument();
});
it('shows validation error for empty email', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: '로그인' }));
expect(screen.getByText('이메일을 입력해주세요')).toBeInTheDocument();
});
it('calls onSubmit with form data', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText('이메일'), 'test@example.com');
await user.type(screen.getByLabelText('비밀번호'), 'password123');
await user.click(screen.getByRole('button', { name: '로그인' }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
});
});
});
비동기 테스트
import { render, screen, waitFor } from '@testing-library/react';
describe('UserProfile', () => {
it('loads and displays user data', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
json: async () => ({ name: '홍길동', email: 'hong@example.com' })
});
render(<UserProfile userId={1} />);
expect(screen.getByText('로딩 중...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('홍길동')).toBeInTheDocument();
});
expect(screen.queryByText('로딩 중...')).not.toBeInTheDocument();
});
it('handles API error gracefully', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
render(<UserProfile userId={1} />);
const errorMessage = await screen.findByText('데이터를 불러올 수 없습니다');
expect(errorMessage).toBeInTheDocument();
});
});
커스텀 훅 테스트
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => { result.current.increment(); });
expect(result.current.count).toBe(1);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.increment();
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
쿼리 우선순위
| 우선순위 | 쿼리 | 사용 시점 |
|---|---|---|
| 1 | getByRole | 접근성 역할로 검색 (권장) |
| 2 | getByLabelText | 폼 요소 |
| 3 | getByPlaceholderText | placeholder가 있는 입력 |
| 4 | getByText | 비대화형 요소 |
| 5 | getByDisplayValue | 입력의 현재 값 |
| 6 | getByTestId | 최후의 수단 |
MSW로 API 모킹
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: '홍길동'
});
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
- 구현 세부사항(state, props, 내부 메서드)이 아닌 사용자 행동을 테스트
getByRole을 우선 사용하면 접근성도 함께 검증됨userEvent는 실제 사용자 상호작용을 더 정확히 시뮬레이션- MSW(Mock Service Worker)로 네트워크 레벨에서 API를 모킹하면 테스트 신뢰도 향상
- 테스트 커버리지보다 중요한 것은 핵심 사용자 흐름의 테스트
댓글 0