Testing Library 완벽 가이드 | React·Vue·사용자 중심 테스트·실전 활용

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

우선순위

  1. getByRole: 접근성 기반 (권장)
  2. getByLabelText: Form 요소
  3. getByPlaceholderText: Placeholder
  4. getByText: 텍스트 내용
  5. getByDisplayValue: Input 값
  6. getByAltText: 이미지
  7. getByTitle: Title 속성
  8. 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 공식 문서에서 권장하는 테스팅 라이브러리입니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3