Testing Library 완벽 가이드 | React·Vue·사용자 중심 테스트·실전 활용
이 글의 핵심
Testing Library로 사용자 중심 테스트를 구현하는 완벽 가이드입니다. Queries, User Events, Async Utils, React/Vue 통합까지 실전 예제로 정리했습니다.
실무 경험 공유: Enzyme에서 Testing Library로 전환하면서, 테스트가 더 견고해지고 리팩토링이 안전해진 경험을 공유합니다.
들어가며: “테스트가 구현에 의존해요”
실무 문제 시나리오
시나리오 1: 리팩토링 시 테스트가 깨져요
구현 세부사항을 테스트합니다. Testing Library는 사용자 관점으로 테스트합니다.
시나리오 2: 테스트가 실제 사용과 달라요
내부 state를 직접 확인합니다. Testing Library는 렌더링 결과를 확인합니다.
시나리오 3: 접근성을 고려하지 못해요
ID, className으로 선택합니다. Testing Library는 Role, Label로 선택합니다.
1. Testing Library란?
핵심 특징
Testing Library는 사용자 중심 테스팅 라이브러리입니다.
주요 장점:
- 사용자 중심: 구현이 아닌 동작 테스트
- 접근성: Role, Label 기반
- 견고함: 리팩토링에 안전
- 프레임워크 지원: React, Vue, Angular
- Jest/Vitest 통합: 완벽한 호환
2. 설치 및 설정
React
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
Vue
npm install -D @testing-library/vue
3. 기본 테스트 (React)
// src/components/Button.tsx
interface ButtonProps {
label: string;
onClick?: () => void;
}
export default function Button({ label, onClick }: ButtonProps) {
return <button onClick={onClick}>{label}</button>;
}
// src/components/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import Button from './Button';
test('renders button', () => {
render(<Button label="Click me" />);
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
test('handles click', async () => {
const user = userEvent.setup();
const handleClick = jest.fn();
render(<Button label="Click me" onClick={handleClick} />);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
4. Queries
우선순위
- getByRole: 접근성 기반 (권장)
- getByLabelText: Form 요소
- getByPlaceholderText: Placeholder
- getByText: 텍스트 내용
- getByDisplayValue: Input 값
- getByAltText: 이미지
- getByTitle: Title 속성
- getByTestId: data-testid (최후)
예제
// getByRole
screen.getByRole('button', { name: 'Submit' });
screen.getByRole('textbox', { name: 'Email' });
screen.getByRole('heading', { level: 1 });
// getByLabelText
screen.getByLabelText('Email');
// getByPlaceholderText
screen.getByPlaceholderText('Enter email');
// getByText
screen.getByText('Hello World');
screen.getByText(/hello/i);
// getByTestId
screen.getByTestId('custom-element');
5. Query Variants
// getBy: 즉시 찾음 (없으면 에러)
screen.getByRole('button');
// queryBy: 즉시 찾음 (없으면 null)
screen.queryByRole('button');
// findBy: 비동기 대기 (없으면 에러)
await screen.findByRole('button');
// getAllBy: 여러 개
screen.getAllByRole('listitem');
6. User Events
import userEvent from '@testing-library/user-event';
test('user interactions', async () => {
const user = userEvent.setup();
render(<LoginForm />);
// 타이핑
await user.type(screen.getByLabelText('Email'), '[email protected]');
// 클릭
await user.click(screen.getByRole('button', { name: 'Submit' }));
// 선택
await user.selectOptions(screen.getByLabelText('Country'), 'US');
// 체크박스
await user.click(screen.getByRole('checkbox', { name: 'Agree' }));
// 파일 업로드
const file = new File(['hello'], 'hello.png', { type: 'image/png' });
const input = screen.getByLabelText('Upload');
await user.upload(input, file);
});
7. 비동기 테스트
waitFor
import { render, screen, waitFor } from '@testing-library/react';
test('loads users', async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText('John')).toBeInTheDocument();
});
});
findBy
test('loads users', async () => {
render(<UserList />);
expect(await screen.findByText('John')).toBeInTheDocument();
});
8. Form 테스트
// src/components/LoginForm.tsx
export default function LoginForm({ onSubmit }: { onSubmit: (data: any) => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="email">Email</label>
<input id="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<label htmlFor="password">Password</label>
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
// src/components/LoginForm.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';
test('submits form', async () => {
const user = userEvent.setup();
const handleSubmit = jest.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText('Email'), '[email protected]');
await user.type(screen.getByLabelText('Password'), 'password123');
await user.click(screen.getByRole('button', { name: 'Submit' }));
expect(handleSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'password123',
});
});
9. Vue 통합
// src/components/Button.vue
<script setup lang="ts">
defineProps<{
label: string;
}>();
const emit = defineEmits<{
click: [];
}>();
</script>
<template>
<button @click="emit('click')">{{ label }}</button>
</template>
// src/components/Button.test.ts
import { render, screen } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import Button from './Button.vue';
test('renders button', () => {
render(Button, { props: { label: 'Click me' } });
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
});
test('handles click', async () => {
const user = userEvent.setup();
const { emitted } = render(Button, { props: { label: 'Click me' } });
await user.click(screen.getByRole('button'));
expect(emitted().click).toHaveLength(1);
});
정리 및 체크리스트
핵심 요약
- Testing Library: 사용자 중심 테스팅
- Queries: Role, Label 기반
- User Events: 실제 사용자 시뮬레이션
- Async Utils: 비동기 대기
- 프레임워크 지원: React, Vue, Angular
- Jest/Vitest 통합: 완벽한 호환
구현 체크리스트
- Testing Library 설치
- 기본 테스트 작성
- Queries 활용
- User Events 사용
- 비동기 테스트
- Form 테스트
- Vue/React 통합
- CI/CD 통합
같이 보면 좋은 글
- Jest 완벽 가이드
- Cypress 완벽 가이드
- React 완벽 가이드
이 글에서 다루는 키워드
Testing Library, React, Vue, Testing, User-Centric, Jest, Vitest
자주 묻는 질문 (FAQ)
Q. Enzyme과 비교하면 어떤가요?
A. Testing Library가 더 사용자 중심적이고 견고합니다. Enzyme은 더 이상 활발히 개발되지 않습니다.
Q. E2E 테스트도 가능한가요?
A. 아니요, 단위/통합 테스트에 적합합니다. E2E는 Cypress나 Playwright를 사용하세요.
Q. 학습 곡선은 어떤가요?
A. 매우 낮습니다. 사용자 관점으로 생각하면 됩니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, React 공식 문서에서 권장하는 테스팅 라이브러리입니다.