| name | testing |
| description | Jest/Vitest + React Testing Library 핵심 패턴, 캡슐화 기반 컴포넌트 테스트 전략 |
Testing — Jest/Vitest + React Testing Library
소스: https://testing-library.com/docs/react-testing-library/intro | https://vitest.dev/guide/
검증일: 2026-08-26 (최초 2026-04-01 · 08-26 freshness 재검증: RTL 16부터 @testing-library/dom이 peerDependency로 분리되어 설치 커맨드에 명시 추가. Vitest 4.1 stable·RTL 16.3 기준 나머지 패턴 VERIFIED)
Jest vs Vitest 선택 기준
| Jest | Vitest |
|---|
| 적합한 환경 | CRA, Next.js (기본 설정) | Vite, 모노레포 |
| 설정 복잡도 | Next.js에서 jest.config + babel/swc 설정 필요 | vite.config에 통합, 설정 최소 |
| 실행 속도 | 보통 | 빠름 (Vite HMR 활용) |
| API 호환성 | - | Jest API 100% 호환 (vi = jest) |
결론: Vite 기반이면 Vitest, Next.js면 Jest (또는 Next.js 공식 jest 설정 사용).
설정
Jest + Next.js
pnpm add -D jest jest-environment-jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event
const nextJest = require('next/jest')
const createJestConfig = nextJest({ dir: './' })
module.exports = createJestConfig({
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
})
import '@testing-library/jest-dom'
Vitest + React
pnpm add -D vitest @vitest/ui jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
globals: true,
},
})
import '@testing-library/jest-dom'
핵심 원칙: 구현이 아닌 동작을 테스트
expect(component.state.isOpen).toBe(true)
expect(wrapper.find('.dropdown-menu')).toHaveLength(1)
expect(screen.getByRole('listbox')).toBeVisible()
expect(screen.getByText('옵션 1')).toBeInTheDocument()
컴포넌트 테스트 패턴
기본 구조
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.getByRole('button', { name: '저장' })).toBeInTheDocument()
})
it('클릭 시 onClick이 호출된다', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(<Button onClick={onClick}>저장</Button>)
await user.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalledTimes(1)
})
it(, () => {
user = userEvent.()
onClick = vi.()
()
user.(screen.())
(onClick)..()
})
})
캡슐화된 컴포넌트 테스트 — 내부 노출 없이 검증
describe('Dropdown', () => {
it('트리거 클릭 시 옵션 목록이 열린다', async () => {
const user = userEvent.setup()
render(<Dropdown options={['사과', '배', '포도']} />)
expect(screen.queryByRole('listbox')).not.toBeInTheDocument()
await user.click(screen.getByRole('button'))
expect(screen.getByRole('listbox')).toBeVisible()
expect(screen.getByText('사과')).toBeInTheDocument()
})
it('옵션 선택 시 onChange가 선택값과 함께 호출된다', async () => {
const user = userEvent.setup()
const onChange = vi.fn()
render(<Dropdown options={['', '']} = />)
user.(screen.())
user.(screen.())
(onChange).()
})
})
커스텀 훅 테스트
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 호출 시 1 증가한다', () => {
const { result } = renderHook(() => useCounter(0))
act(() => { result.current.increment() })
expect(result.current.count).toBe(1)
})
})
비동기 테스트
import { render, screen, waitFor } from '@testing-library/react'
vi.mock('../api/users', () => ({
fetchUser: vi.fn().mockResolvedValue({ name: '홍길동', email: 'hong@test.com' }),
}))
describe('UserProfile', () => {
it('사용자 정보를 불러와서 표시한다', async () => {
render(<UserProfile userId="1" />)
expect(screen.getByText('로딩 중...')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('홍길동')).toBeInTheDocument()
})
expect(screen.getByText('hong@test.com')).toBeInTheDocument()
})
it('API 실패 시 에러 메시지를 표시한다', async () => {
const { fetchUser } = await import()
vi.(fetchUser).( ())
()
( {
(screen.()).()
})
})
})
쿼리 우선순위 (RTL 권장 순서)
| 우선순위 | 쿼리 | 사용 시점 |
|---|
| 1 | getByRole | 접근성 역할 (button, heading, listbox 등) |
| 2 | getByLabelText | 폼 필드 (label과 연결된 input) |
| 3 | getByPlaceholderText | placeholder가 있는 input |
| 4 | getByText | 텍스트 콘텐츠 |
| 5 | getByDisplayValue | 현재 값이 있는 input/select |
| 6 | getByAltText | img alt 텍스트 |
| 7 | getByTitle | title 속성 |
| 8 | getByTestId | data-testid (최후 수단) |
테스트 파일 배치
components/
└── Button/
├── Button.tsx
├── Button.module.scss
└── Button.test.tsx ← 컴포넌트 옆에 배치 (colocated)
__tests__/ 폴더 분리보다 컴포넌트 옆에 두는 것을 권장. 파일 이동 시 테스트도 함께 이동됨.