
Testing
- 357 installs
- 4 repo stars
- Updated April 7, 2026
- dalestudy/skills
testing is a Claude Code skill that writes and runs unit, integration, and regression test suites for dalestudy/skills codebases so developers can catch defects and validate behavior before release.
About
testing is a general-purpose Claude Code skill from dalestudy/skills focused on writing, organizing, and executing test coverage across a project. The skill structures unit, integration, and regression suites, runs them against the current codebase, and helps surface defects before release. Developers reach for testing when dalestudy/skills projects need new test cases, better suite organization, or a pre-release validation pass without a framework-specific bootstrap guide. The workflow spans authoring tests, arranging them into logical suites, and executing runs to confirm expected behavior holds across recent changes.
- Unit and integration test patterns
- Regression prevention workflows
- Test suite organization
- Assertion and mocking guidance
- Pre-release quality checks
Testing by the numbers
- 357 all-time installs (skills.sh)
- Ranked #665 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dalestudy/skills --skill testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 357 |
|---|---|
| repo stars | ★ 4 |
| Last updated | April 7, 2026 |
| Repository | dalestudy/skills ↗ |
How do you structure and run tests before release?
Write and run unit, integration, and regression tests; structure suites and catch defects before release in dalestudy/skills codebases.
Who is it for?
Developers working in dalestudy/skills repositories who need to add or reorganize test coverage and run pre-release validation across unit, integration, and regression layers.
Skip if: Teams needing platform-specific test bootstrapping such as Android Gradle modules or Symfony Panther E2E setups where a specialized testing-setup skill is more appropriate.
When should I use this skill?
A dalestudy/skills codebase needs new tests written, suites reorganized, or a regression run before a release merge.
What you get
Unit, integration, and regression test files, organized test suites, and executed test run results.
- Test files
- Structured test suites
- Test run results
Files
Testing Library
React Testing Library 기반 테스트 작성 모범 관례 및 안티패턴 회피 가이드.
핵심 원칙
Testing Library의 철학: 사용자가 사용하는 방식대로 테스트하라
1. 접근성 기반 쿼리 우선 - 실제 사용자가 요소를 찾는 방식 사용 2. 구현 세부사항 테스트 금지 - 컴포넌트 내부 상태/메서드 직접 접근 지양 3. 실제 사용자 행동 시뮬레이션 - userEvent 사용, fireEvent 지양 4. 비동기 처리 명시적 대기 - waitFor, findBy 활용
쿼리 우선순위
Testing Library는 다양한 쿼리를 제공하지만, 접근성과 사용자 경험을 반영하는 순서로 사용해야 함.
권장 쿼리 순서 (높음 → 낮음)
1. `getByRole` (최우선) - 스크린 리더가 인식하는 방식 2. `getByLabelText` - 폼 요소 (label과 연결된 input) 3. `getByPlaceholderText` - placeholder가 명확한 경우 4. `getByText` - 텍스트 콘텐츠로 검색 5. `getByDisplayValue` - 현재 입력된 값으로 검색 (폼 요소) 6. `getByAltText` - 이미지 alt 속성 7. `getByTitle` - title 속성 (tooltip 등) 8. `getByTestId` (최후 수단) - 다른 방법이 불가능할 때만 사용
상세 가이드: references/query-priority.md
사용자 상호작용 테스트
userEvent 사용 (권장)
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('사용자가 폼을 제출할 수 있다', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(screen.getByRole('textbox', { name: /이메일/i }), 'user@example.com');
await user.type(screen.getByLabelText(/비밀번호/i), 'password123');
await user.click(screen.getByRole('button', { name: /로그인/i }));
expect(await screen.findByText(/환영합니다/i)).toBeInTheDocument();
});핵심:
userEvent.setup()호출 후 사용- 모든 user 메서드는
await필수 - 실제 브라우저 이벤트 순서 재현 (focus, keydown, keyup 등)
fireEvent 지양
// ❌ 나쁜 예 - fireEvent 사용
fireEvent.click(button);
fireEvent.change(input, { target: { value: "text" } });
// ✅ 좋은 예 - userEvent 사용
await user.click(button);
await user.type(input, "text");상세 가이드: references/user-events.md
비동기 처리
findBy 쿼리 (권장)
// ✅ 좋은 예 - findBy 사용
const successMessage = await screen.findByText(/저장되었습니다/i);
expect(successMessage).toBeInTheDocument();findBy = getBy + waitFor 조합 (자동으로 요소 나타날 때까지 대기)
waitFor 사용
// 복잡한 비동기 검증
await waitFor(() => {
expect(screen.getByRole("alert")).toHaveTextContent("성공");
});
// 여러 조건 검증
await waitFor(() => {
expect(mockFn).toHaveBeenCalledTimes(1);
expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
});안티패턴
// ❌ 나쁜 예 - 임의의 timeout
await new Promise((resolve) => setTimeout(resolve, 1000));
// ❌ 나쁜 예 - act() 수동 사용 (보통 불필요)
await act(async () => {
// ...
});
// ✅ 좋은 예 - findBy 또는 waitFor
await screen.findByText(/완료/i);상세 가이드: references/async-patterns.md
자주 하는 실수
1. 구현 세부사항 테스트
// ❌ 나쁜 예 - 내부 상태 접근
expect(component.state.isOpen).toBe(true);
wrapper.find(".internal-class").simulate("click");
// ✅ 좋은 예 - 사용자 관점 검증
expect(screen.getByRole("dialog")).toBeVisible();
await user.click(screen.getByRole("button", { name: /열기/i }));2. container 쿼리 사용
// ❌ 나쁜 예 - container.querySelector
const { container } = render(<MyComponent />);
const button = container.querySelector('.my-button');
// ✅ 좋은 예 - screen 쿼리
const button = screen.getByRole('button', { name: /제출/i });3. 불필요한 waitFor
// ❌ 나쁜 예 - 동기 요소에 waitFor
await waitFor(() => {
expect(screen.getByText("Hello")).toBeInTheDocument();
});
// ✅ 좋은 예 - 동기 요소는 즉시 검증
expect(screen.getByText("Hello")).toBeInTheDocument();4. getBy* + toBeInTheDocument() 제거
getBy*는 요소를 못 찾으면 throw하므로 toBeInTheDocument()는 기술적으로 중복이다. 하지만 제거하지 마라 — 리팩토링 후 남은 쿼리가 아니라 의도적인 존재 검증임을 코드 독자에게 전달하는 역할을 한다.
// ❌ 나쁜 예 - assertion 없이 쿼리만 남김
screen.getByRole("button", { name: /제출/i });
// ✅ 좋은 예 - 명시적 assertion으로 의도 전달
expect(screen.getByRole("button", { name: /제출/i })).toBeInTheDocument();단, 존재 검증이 아닌 다른 속성을 검증할 때는 toBeInTheDocument()를 추가할 필요 없다:
// ✅ 좋은 예 - 다른 matcher로 충분
expect(screen.getByRole("button", { name: /제출/i })).toBeDisabled();
// ❌ 나쁜 예 - 중복 assertion
expect(screen.getByRole("button", { name: /제출/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /제출/i })).toBeDisabled();전체 안티패턴 목록: references/common-mistakes.md
Vitest + MSW 설정
테스트 환경 설정이 필요한 경우 다음 템플릿 참조:
assets/vitest.config.ts- Vitest 설정 (React 플러그인, 커버리지 포함)assets/test-setup.ts- Vitest 글로벌 설정assets/msw-setup.ts- MSW 핸들러 및 서버 설정
// MSW (Mock Service Worker) 설정
// tests/mocks/server.ts
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// API 핸들러 정의
export const handlers = [
// 예시: GET /api/users
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
]);
}),
// 예시: POST /api/login
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json();
if (email === 'user@example.com' && password === 'password123') {
return HttpResponse.json({
token: 'fake-jwt-token',
user: { id: 1, email },
});
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
// 예시: 에러 응답
http.get('/api/error', () => {
return HttpResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}),
];
// MSW 서버 생성
export const server = setupServer(...handlers);
// 테스트 내에서 핸들러 오버라이드 예시:
// import { server } from './mocks/server';
// import { http, HttpResponse } from 'msw';
//
// test('에러 처리', async () => {
// server.use(
// http.get('/api/users', () => {
// return HttpResponse.json({ error: 'Failed' }, { status: 500 });
// })
// );
//
// render(<UserList />);
// expect(await screen.findByRole('alert')).toHaveTextContent(/오류/i);
// });
// Vitest 글로벌 테스트 설정
// tests/setup.ts
import { expect, afterEach, beforeAll, afterAll } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
import { server } from './mocks/server';
// jest-dom matchers 확장
expect.extend(matchers);
// 각 테스트 후 cleanup (자동이지만 명시적으로 설정)
afterEach(() => {
cleanup();
});
// MSW 서버 설정
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
// Jest DOM matchers 사용
globals: true,
// jsdom 환경 (브라우저 시뮬레이션)
environment: 'jsdom',
// 테스트 설정 파일
setupFiles: './tests/setup.ts',
// 커버리지 설정
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.config.ts',
'**/*.d.ts',
],
},
// include/exclude 패턴
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
},
});
Async Patterns
Testing Library 비동기 처리 패턴 및 안티패턴 가이드.
핵심 원칙
1. findBy 쿼리 우선 - getBy + waitFor 조합을 자동화 2. waitFor 사용 - 복잡한 비동기 조건 검증 3. 임의의 timeout 금지 - setTimeout, sleep 등 사용 지양 4. act() 수동 사용 지양 - Testing Library가 자동 처리
findBy 쿼리 (권장)
비동기로 나타나는 요소를 기다리는 가장 간단한 방법.
기본 사용
// ✅ 좋은 예 - findBy 사용
const successMessage = await screen.findByText(/저장되었습니다/i);
expect(successMessage).toBeInTheDocument();
// ❌ 나쁜 예 - getBy + waitFor
await waitFor(() => {
expect(screen.getByText(/저장되었습니다/i)).toBeInTheDocument();
});findBy = getBy + waitFor
findBy는 내부적으로 다음과 같이 동작:
// findByText는 이것과 동일
await waitFor(() => screen.getByText(/텍스트/i));기본 타임아웃: 1000ms (1초)
복수 요소 - findAllBy
// 비동기로 나타나는 여러 요소
const items = await screen.findAllByRole('listitem');
expect(items).toHaveLength(3);waitFor
복잡한 비동기 조건을 검증할 때 사용.
기본 사용
await waitFor(() => {
expect(mockFn).toHaveBeenCalledTimes(1);
});
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent('성공');
});여러 조건 검증
await waitFor(() => {
// 모든 조건이 true가 될 때까지 대기
expect(screen.getByRole('button')).not.toBeDisabled();
expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
expect(mockApi).toHaveBeenCalled();
});옵션
await waitFor(
() => {
expect(element).toBeInTheDocument();
},
{
timeout: 3000, // 최대 대기 시간 (기본: 1000ms)
interval: 50, // 재시도 간격 (기본: 50ms)
}
);사용 사례별 패턴
1. API 호출 후 UI 업데이트
test('데이터를 불러와서 화면에 표시한다', async () => {
const user = userEvent.setup();
render(<UserList />);
await user.click(screen.getByRole('button', { name: /불러오기/i }));
// ✅ findBy 사용
expect(await screen.findByText(/John Doe/i)).toBeInTheDocument();
// ❌ 임의의 timeout 사용 금지
// await new Promise(resolve => setTimeout(resolve, 1000));
// expect(screen.getByText(/John Doe/i)).toBeInTheDocument();
});2. 로딩 상태 → 성공 상태
test('로딩 후 데이터를 표시한다', async () => {
render(<AsyncComponent />);
// 초기 로딩 상태 확인
expect(screen.getByText(/로딩 중/i)).toBeInTheDocument();
// ✅ 로딩이 사라질 때까지 대기
await waitFor(() => {
expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
});
// 데이터 표시 확인
expect(screen.getByText(/데이터/i)).toBeInTheDocument();
});또는 waitForElementToBeRemoved 사용:
import { waitForElementToBeRemoved } from '@testing-library/react';
test('로딩 후 데이터를 표시한다', async () => {
render(<AsyncComponent />);
const loader = screen.getByText(/로딩 중/i);
// ✅ 요소가 DOM에서 제거될 때까지 대기
await waitForElementToBeRemoved(loader);
expect(screen.getByText(/데이터/i)).toBeInTheDocument();
});3. 에러 상태 표시
test('에러가 발생하면 에러 메시지를 표시한다', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Server Error' }, { status: 500 });
})
);
const user = userEvent.setup();
render(<UserList />);
await user.click(screen.getByRole('button', { name: /불러오기/i }));
// ✅ findBy로 에러 메시지 대기
const errorAlert = await screen.findByRole('alert');
expect(errorAlert).toHaveTextContent(/오류가 발생했습니다/i);
});4. 폼 제출 후 리다이렉트
test('로그인 성공 시 대시보드로 이동한다', async () => {
const user = userEvent.setup();
render(<LoginPage />);
await user.type(screen.getByRole('textbox', { name: /이메일/i }), 'user@example.com');
await user.type(screen.getByLabelText(/비밀번호/i), 'password');
await user.click(screen.getByRole('button', { name: /로그인/i }));
// ✅ 새 페이지 요소 대기
expect(await screen.findByRole('heading', { name: /대시보드/i })).toBeInTheDocument();
});5. 디바운스 입력
test('디바운스된 검색이 동작한다', async () => {
const user = userEvent.setup();
render(<SearchBar debounceMs={500} />);
const searchInput = screen.getByRole('searchbox');
// 빠르게 입력
await user.type(searchInput, 'React');
// ✅ 디바운스 후 결과 대기
expect(await screen.findByText(/검색 결과: React/i)).toBeInTheDocument();
});6. 애니메이션 후 상태 변경
test('모달이 애니메이션 후 닫힌다', async () => {
const user = userEvent.setup();
render(<Modal />);
await user.click(screen.getByRole('button', { name: /닫기/i }));
// ✅ 애니메이션 후 DOM에서 제거 대기
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});MSW (Mock Service Worker) 통합
설정
// tests/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]);
}),
];
// tests/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);// tests/setup.ts
import { server } from './mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());사용 예시
import { server } from './mocks/server';
import { http, HttpResponse } from 'msw';
test('사용자 목록을 불러온다', async () => {
render(<UserList />);
// ✅ MSW가 모킹한 데이터가 나타날 때까지 대기
expect(await screen.findByText(/John Doe/i)).toBeInTheDocument();
expect(await screen.findByText(/Jane Smith/i)).toBeInTheDocument();
});
test('에러 발생 시 에러 메시지를 표시한다', async () => {
// 특정 테스트에서만 에러 응답 반환
server.use(
http.get('/api/users', () => {
return HttpResponse.json({ error: 'Failed' }, { status: 500 });
})
);
render(<UserList />);
// ✅ 에러 알림 대기
expect(await screen.findByRole('alert')).toHaveTextContent(/오류/i);
});지연 시뮬레이션
import { delay, http, HttpResponse } from 'msw';
test('로딩 상태가 표시된다', async () => {
server.use(
http.get('/api/users', async () => {
await delay(200); // 200ms 지연
return HttpResponse.json([{ id: 1, name: 'John' }]);
})
);
render(<UserList />);
// 로딩 표시 확인
expect(screen.getByText(/로딩 중/i)).toBeInTheDocument();
// ✅ 데이터 로딩 후 대기
expect(await screen.findByText(/John/i)).toBeInTheDocument();
// 로딩 사라짐 확인
expect(screen.queryByText(/로딩 중/i)).not.toBeInTheDocument();
});안티패턴
❌ 임의의 timeout
// ❌ 나쁜 예 - 임의의 대기 시간
await new Promise(resolve => setTimeout(resolve, 1000));
expect(screen.getByText(/완료/i)).toBeInTheDocument();
// ✅ 좋은 예 - findBy 사용
expect(await screen.findByText(/완료/i)).toBeInTheDocument();문제점:
- 불필요한 테스트 지연 (항상 1초 대기)
- 환경에 따라 실패 가능 (느린 CI에서 1초로 부족할 수 있음)
❌ act() 수동 사용
// ❌ 나쁜 예 - 수동 act() 사용
await act(async () => {
fireEvent.click(button);
});
// ✅ 좋은 예 - userEvent 사용 (자동으로 act 처리)
await user.click(button);언제 act 경고가 발생하나?
- 상태 업데이트가 테스트 외부에서 발생할 때
- 보통 Testing Library 메서드가 자동 처리하므로 수동 사용 불필요
act 경고 해결법: 1. await 누락 확인 2. findBy 또는 waitFor 사용 3. 여전히 발생하면 코드 문제 확인 (테스트 외부 상태 업데이트 등)
❌ 동기 요소에 waitFor
// ❌ 나쁜 예 - 이미 렌더링된 요소에 waitFor
await waitFor(() => {
expect(screen.getByText('Hello')).toBeInTheDocument();
});
// ✅ 좋은 예 - 즉시 검증
expect(screen.getByText('Hello')).toBeInTheDocument();❌ waitFor 안에서 side effect
// ❌ 나쁜 예 - waitFor 콜백 안에서 클릭
await waitFor(() => {
fireEvent.click(button); // side effect!
expect(something).toBe(true);
});
// ✅ 좋은 예 - waitFor 밖에서 실행
await user.click(button);
await waitFor(() => {
expect(something).toBe(true);
});waitFor는 여러 번 재실행됨 → side effect 있으면 여러 번 발생
❌ queryBy로 존재 확인
// ❌ 나쁜 예 - queryBy로 비동기 요소 찾기
const element = screen.queryByText(/완료/i);
expect(element).toBeInTheDocument(); // null이면 실패
// ✅ 좋은 예 - findBy 사용
expect(await screen.findByText(/완료/i)).toBeInTheDocument();디버깅
screen.debug()
test('디버깅', async () => {
render(<MyComponent />);
// 현재 DOM 출력
screen.debug();
await user.click(button);
// 변경 후 DOM 출력
screen.debug();
});logRoles
import { logRoles } from '@testing-library/react';
test('role 디버깅', () => {
const { container } = render(<MyComponent />);
// 모든 role 출력
logRoles(container);
});waitFor 디버깅
await waitFor(
() => {
console.log('Checking...');
expect(element).toBeInTheDocument();
},
{ timeout: 3000, interval: 100 }
);타임아웃 설정
전역 설정
// tests/setup.ts
import { configure } from '@testing-library/react';
configure({ asyncUtilTimeout: 3000 }); // 기본: 1000ms개별 설정
// 특정 findBy
await screen.findByText(/텍스트/i, {}, { timeout: 3000 });
// waitFor
await waitFor(
() => expect(element).toBeInTheDocument(),
{ timeout: 5000 }
);요약
비동기 처리 우선순위: 1. ✅ findBy - 요소가 나타날 때까지 대기 2. ✅ waitFor - 복잡한 조건 검증 3. ✅ waitForElementToBeRemoved - 요소 제거 대기
피해야 할 것: 1. ❌ setTimeout, sleep 등 임의 대기 2. ❌ 수동 act() 호출 3. ❌ 동기 요소에 waitFor 사용 4. ❌ waitFor 콜백 내 side effect
핵심 원칙:
- 실제 사용자처럼 대기 (요소가 나타날 때까지)
- Testing Library가 자동 처리하므로 수동 최적화 불필요
- MSW로 API 모킹하면 일관된 비동기 테스트 가능
Common Mistakes
Testing Library 사용 시 자주 하는 실수와 해결법.
1. 구현 세부사항 테스트
❌ 안티패턴: 내부 상태/메서드 접근
// ❌ 나쁜 예 - React 내부 상태 테스트
const wrapper = shallow(<Counter />);
expect(wrapper.state('count')).toBe(0);
wrapper.instance().increment();
expect(wrapper.state('count')).toBe(1);
// ❌ 나쁜 예 - 클래스명으로 요소 찾기
const button = container.querySelector('.increment-button');
fireEvent.click(button);✅ 올바른 방법: 사용자 관점 테스트
// ✅ 좋은 예 - 사용자가 보는 결과 테스트
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /증가/i }));
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();원칙: 사용자가 볼 수 없는 것은 테스트하지 않는다.
---
2. container.querySelector 사용
❌ 안티패턴: DOM 직접 쿼리
// ❌ 나쁜 예
const { container } = render(<LoginForm />);
const emailInput = container.querySelector('#email');
const submitButton = container.querySelector('button[type="submit"]');✅ 올바른 방법: screen 쿼리
// ✅ 좋은 예
render(<LoginForm />);
const emailInput = screen.getByRole('textbox', { name: /이메일/i });
const submitButton = screen.getByRole('button', { name: /로그인/i });이유:
screen쿼리는 접근성 기반- 스크린 리더 사용자 경험 반영
- 리팩토링에 강함
---
3. data-testid 남용
❌ 안티패턴: testid 과도 사용
// ❌ 나쁜 예
<button data-testid="submit-button">제출</button>
<input data-testid="email-input" aria-label="이메일" />
screen.getByTestId('submit-button');
screen.getByTestId('email-input');✅ 올바른 방법: 접근성 쿼리 우선
// ✅ 좋은 예
<button>제출</button>
<input aria-label="이메일" />
screen.getByRole('button', { name: /제출/i });
screen.getByRole('textbox', { name: /이메일/i });testid 사용해도 되는 경우:
- 동적 콘텐츠로 role/text 불명확
- 서드파티 라이브러리 (접근성 속성 없음)
- 다른 모든 쿼리 불가능
---
4. fireEvent 사용
❌ 안티패턴: fireEvent로 사용자 이벤트 시뮬레이션
// ❌ 나쁜 예
const input = screen.getByRole('textbox');
fireEvent.change(input, { target: { value: 'Hello' } });
fireEvent.click(screen.getByRole('button'));✅ 올바른 방법: userEvent 사용
// ✅ 좋은 예
const user = userEvent.setup();
const input = screen.getByRole('textbox');
await user.type(input, 'Hello');
await user.click(screen.getByRole('button'));차이점:
fireEvent: 단일 이벤트만 발생userEvent: 실제 사용자 행동 재현 (focus → keydown → keyup → change...)
fireEvent 사용해도 되는 경우:
- userEvent가 지원하지 않는 이벤트 (
scroll,resize등)
---
5. 임의의 timeout
❌ 안티패턴: setTimeout으로 대기
// ❌ 나쁜 예
await new Promise(resolve => setTimeout(resolve, 1000));
expect(screen.getByText(/완료/i)).toBeInTheDocument();
// ❌ 나쁜 예
await sleep(500);
expect(mockFn).toHaveBeenCalled();✅ 올바른 방법: findBy 또는 waitFor
// ✅ 좋은 예
expect(await screen.findByText(/완료/i)).toBeInTheDocument();
// ✅ 좋은 예
await waitFor(() => {
expect(mockFn).toHaveBeenCalled();
});이유:
- 불필요한 대기 시간 (항상 고정 시간만큼 대기)
- 환경에 따라 불안정 (CI에서 더 느릴 수 있음)
---
6. act() 수동 사용
❌ 안티패턴: 수동 act() 래핑
// ❌ 나쁜 예
await act(async () => {
fireEvent.click(button);
});
// ❌ 나쁜 예
act(() => {
render(<MyComponent />);
});✅ 올바른 방법: Testing Library 메서드 사용
// ✅ 좋은 예 - userEvent가 자동으로 act 처리
await user.click(button);
// ✅ 좋은 예 - render가 자동으로 act 처리
render(<MyComponent />);act 경고 발생 시 해결법: 1. await 누락 확인 2. findBy 또는 waitFor 사용 3. 비동기 상태 업데이트가 테스트 외부에서 발생하는지 확인
---
7. 동기 요소에 waitFor
❌ 안티패턴: 불필요한 waitFor
// ❌ 나쁜 예 - 이미 렌더링된 요소
await waitFor(() => {
expect(screen.getByText('Hello')).toBeInTheDocument();
});
// ❌ 나쁜 예
await waitFor(() => {
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});✅ 올바른 방법: 즉시 검증
// ✅ 좋은 예 - 동기 요소는 바로 검증
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveTextContent('Click me');waitFor 사용해야 하는 경우:
- 비동기로 나타나는 요소
- 상태가 변경될 때까지 대기
---
8. waitFor 안에서 side effect
❌ 안티패턴: waitFor 콜백에서 이벤트 발생
// ❌ 나쁜 예 - waitFor 콜백이 여러 번 실행됨
await waitFor(() => {
fireEvent.click(button); // 여러 번 클릭됨!
expect(count).toBe(1);
});✅ 올바른 방법: waitFor 밖에서 실행
// ✅ 좋은 예
await user.click(button);
await waitFor(() => {
expect(count).toBe(1);
});이유: waitFor 콜백은 조건이 만족될 때까지 여러 번 재실행됨
---
9. getBy로 비동기 요소 찾기
❌ 안티패턴: getBy로 비동기 요소 검증
// ❌ 나쁜 예 - API 호출 후 즉시 getBy 사용
await user.click(screen.getByRole('button', { name: /불러오기/i }));
expect(screen.getByText(/John Doe/i)).toBeInTheDocument(); // 에러 발생 가능✅ 올바른 방법: findBy 사용
// ✅ 좋은 예
await user.click(screen.getByRole('button', { name: /불러오기/i }));
expect(await screen.findByText(/John Doe/i)).toBeInTheDocument();---
10. queryBy로 존재 확인
❌ 안티패턴: queryBy로 비동기 요소 찾기
// ❌ 나쁜 예 - queryBy는 비동기 대기 안 함
const element = screen.queryByText(/완료/i);
expect(element).toBeInTheDocument(); // null이면 실패✅ 올바른 방법: 용도에 맞는 쿼리 사용
// ✅ 좋은 예 - 비동기 요소는 findBy
expect(await screen.findByText(/완료/i)).toBeInTheDocument();
// ✅ 좋은 예 - 없음을 검증할 때만 queryBy
expect(screen.queryByText(/에러/i)).not.toBeInTheDocument();쿼리 선택 기준:
getBy- 요소가 이미 있음queryBy- 요소가 없음을 검증findBy- 요소가 비동기로 나타남
---
11. 접근성 무시
❌ 안티패턴: 접근성 속성 누락
// ❌ 나쁜 예 - label 없는 input
<input type="text" placeholder="이메일 입력" />
// 테스트에서 placeholder로 찾아야 함 (안티패턴)
screen.getByPlaceholderText(/이메일 입력/i);✅ 올바른 방법: 접근성 속성 추가
// ✅ 좋은 예 - label 연결
<label htmlFor="email">이메일</label>
<input id="email" type="text" />
// 또는 aria-label
<input type="text" aria-label="이메일" />
// 테스트
screen.getByRole('textbox', { name: /이메일/i });Testing Library를 사용하면 자연스럽게 접근성 개선됨
---
12. 불필요한 cleanup
❌ 안티패턴: 수동 cleanup
// ❌ 나쁜 예 - 수동 cleanup
import { cleanup } from '@testing-library/react';
afterEach(() => {
cleanup();
});✅ 올바른 방법: 자동 cleanup (기본값)
// ✅ 좋은 예 - cleanup은 자동으로 실행됨
// 아무것도 안 해도 됨이유: Testing Library가 각 테스트 후 자동으로 cleanup 실행
---
13. wrapper 재사용
❌ 안티패턴: render 결과 재사용
// ❌ 나쁜 예
const { rerender } = render(<Counter initialCount={0} />);
rerender(<Counter initialCount={5} />);✅ 올바른 방법: 새로 render
// ✅ 좋은 예 - 대부분의 경우
render(<Counter initialCount={0} />);
// ... 테스트 ...
// 새 테스트
render(<Counter initialCount={5} />);rerender 사용해도 되는 경우:
- props 변경 시 컴포넌트 동작 테스트 (드문 경우)
---
14. waitFor에서 getBy 사용
❌ 안티패턴: waitFor + getBy 조합
// ❌ 나쁜 예 - 장황함
await waitFor(() => {
expect(screen.getByText(/완료/i)).toBeInTheDocument();
});✅ 올바른 방법: findBy 사용
// ✅ 좋은 예 - 간결함
expect(await screen.findByText(/완료/i)).toBeInTheDocument();---
15. 스냅샷 테스트 과용
❌ 안티패턴: 모든 컴포넌트 스냅샷
// ❌ 나쁜 예 - 의미 없는 스냅샷
test('renders correctly', () => {
const { container } = render(<MyComponent />);
expect(container).toMatchSnapshot();
});✅ 올바른 방법: 의미 있는 검증
// ✅ 좋은 예 - 실제 동작 테스트
test('사용자가 버튼을 클릭하면 카운트가 증가한다', async () => {
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: /증가/i }));
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});스냅샷 테스트 사용해도 되는 경우:
- 복잡한 데이터 구조 (JSON, config 등)
- 에러 메시지 형식
---
16. 비동기 테스트에서 await 누락
❌ 안티패턴: await 누락
// ❌ 나쁜 예 - await 없음
test('example', () => {
const user = userEvent.setup();
user.click(button); // await 누락!
expect(screen.getByText(/완료/i)).toBeInTheDocument();
});✅ 올바른 방법: async/await
// ✅ 좋은 예
test('example', async () => {
const user = userEvent.setup();
await user.click(button);
expect(await screen.findByText(/완료/i)).toBeInTheDocument();
});---
17. 잘못된 쿼리 우선순위
❌ 안티패턴: getByText로 버튼 찾기
// ❌ 나쁜 예
const submitButton = screen.getByText(/제출/i);
const emailInput = screen.getByPlaceholderText(/이메일/i);✅ 올바른 방법: getByRole 우선
// ✅ 좋은 예
const submitButton = screen.getByRole('button', { name: /제출/i });
const emailInput = screen.getByRole('textbox', { name: /이메일/i });쿼리 우선순위: 1. getByRole 2. getByLabelText 3. getByPlaceholderText 4. getByText 5. getByTestId (최후 수단)
---
요약 체크리스트
피해야 할 것:
- ❌ 구현 세부사항 테스트 (state, 클래스명)
- ❌
container.querySelector사용 - ❌
data-testid남용 - ❌
fireEvent사용 (userEvent 대신) - ❌ 임의의
setTimeout - ❌ 수동
act()호출 - ❌ 동기 요소에
waitFor - ❌
waitFor안에서 side effect - ❌ 비동기 요소에
getBy사용 - ❌ 접근성 속성 누락
권장 사항:
- ✅ 사용자 관점 테스트
- ✅
screen쿼리 사용 - ✅
getByRole우선 사용 - ✅
userEvent사용 - ✅
findBy또는waitFor사용 - ✅ Testing Library 자동 기능 활용
- ✅ 접근성 개선
핵심 원칙:
"사용자가 사용하는 방식대로 테스트하라"
Query Priority Guide
Testing Library 쿼리 선택 상세 가이드. 접근성과 사용자 경험을 최우선으로 고려한 순서.
1. getByRole (최우선)
사용 시기: 거의 모든 요소 (버튼, 링크, 폼 요소, 헤딩 등)
스크린 리더가 요소를 인식하는 방식과 동일. 가장 강력하고 권장되는 쿼리.
기본 사용
// 버튼
screen.getByRole('button', { name: /제출/i });
screen.getByRole('button', { name: /취소/i });
// 링크
screen.getByRole('link', { name: /더 보기/i });
// 입력 필드
screen.getByRole('textbox', { name: /이메일/i });
screen.getByRole('checkbox', { name: /약관 동의/i });
screen.getByRole('combobox', { name: /국가 선택/i });
// 헤딩
screen.getByRole('heading', { name: /사용자 설정/i, level: 1 });
// 기타
screen.getByRole('alert');
screen.getByRole('dialog');
screen.getByRole('navigation');주요 Role 목록
| 요소 | Role | 예시 |
|---|---|---|
<button> | button | getByRole('button') |
<a> | link | getByRole('link') |
<input type="text"> | textbox | getByRole('textbox') |
<input type="checkbox"> | checkbox | getByRole('checkbox') |
<input type="radio"> | radio | getByRole('radio') |
<select> | combobox | getByRole('combobox') |
<textarea> | textbox | getByRole('textbox') |
<h1> ~ <h6> | heading | getByRole('heading', { level: 1 }) |
<img alt="..."> | img | getByRole('img') |
<nav> | navigation | getByRole('navigation') |
<main> | main | getByRole('main') |
<ul>, <ol> | list | getByRole('list') |
<li> | listitem | getByRole('listitem') |
고급 옵션
// name: 접근 가능한 이름 (텍스트, aria-label, aria-labelledby 등)
screen.getByRole('button', { name: /저장/i });
// level: 헤딩 레벨
screen.getByRole('heading', { level: 2, name: /제목/i });
// checked: 체크 상태
screen.getByRole('checkbox', { checked: true });
// pressed: 토글 버튼 상태
screen.getByRole('button', { pressed: true });
// expanded: 확장/축소 상태
screen.getByRole('button', { expanded: false });
// hidden: 숨겨진 요소 포함
screen.getByRole('button', { hidden: true, name: /숨김/i });2. getByLabelText
사용 시기: 폼 요소 (input, textarea, select)가 <label>과 연결된 경우
// <label htmlFor="email">이메일</label>
// <input id="email" type="email" />
screen.getByLabelText(/이메일/i);
// aria-label 사용
// <input aria-label="검색어 입력" />
screen.getByLabelText(/검색어 입력/i);
// aria-labelledby 사용
// <span id="username-label">사용자명</span>
// <input aria-labelledby="username-label" />
screen.getByLabelText(/사용자명/i);언제 사용하지 말아야 하나?
getByRole('textbox', { name: /.../ })이 더 명시적이므로 보통 role 쿼리 선호- 하지만 label이 명확한 폼 요소에는 여전히 유용
3. getByPlaceholderText
사용 시기: placeholder가 명확하고 유일한 경우 (권장하지 않음)
// <input placeholder="이메일을 입력하세요" />
screen.getByPlaceholderText(/이메일을 입력하세요/i);주의사항:
- placeholder는 접근성이 떨어지므로 label을 대체할 수 없음
- 가능하면
getByRole또는getByLabelText사용 권장
4. getByText
사용 시기: 텍스트 콘텐츠로 요소를 찾을 때 (버튼, 링크, 헤딩 제외)
// 단락, div, span 등
screen.getByText(/환영합니다/i);
screen.getByText(/사용자 목록/i);
// 정확한 매칭
screen.getByText('Hello World', { exact: true });
// 부분 매칭 (함수)
screen.getByText((content, element) => {
return content.startsWith('Error:');
});버튼/링크는 getByRole 사용:
// ❌ 나쁜 예
screen.getByText(/제출/i);
// ✅ 좋은 예
screen.getByRole('button', { name: /제출/i });5. getByDisplayValue
사용 시기: 현재 입력된 값으로 폼 요소를 찾을 때
// <input value="John Doe" />
screen.getByDisplayValue(/John Doe/i);
// <select>
// <option value="kr" selected>한국</option>
// </select>
screen.getByDisplayValue(/한국/i);주의: 초기값이 아닌 현재 값으로 검색함
6. getByAltText
사용 시기: 이미지의 alt 속성으로 검색
// <img alt="프로필 사진" src="..." />
screen.getByAltText(/프로필 사진/i);getByRole('img')와 비교:
// 둘 다 가능
screen.getByAltText(/로고/i);
screen.getByRole('img', { name: /로고/i });
// getByRole이 더 명시적이므로 권장7. getByTitle
사용 시기: title 속성으로 검색 (tooltip 등)
// <button title="닫기">✕</button>
screen.getByTitle(/닫기/i);
// SVG
// <svg title="체크 아이콘"><path /></svg>
screen.getByTitle(/체크 아이콘/i);주의: title은 접근성이 떨어지므로 가능하면 aria-label 사용 권장
8. getByTestId (최후 수단)
사용 시기: 다른 모든 방법이 불가능할 때만 사용
// <div data-testid="custom-element">...</div>
screen.getByTestId('custom-element');사용해야 하는 경우:
- 동적 콘텐츠로 텍스트/role이 불명확
- 서드파티 라이브러리 요소 (접근성 속성 없음)
- 시각적으로만 구분되는 요소
피해야 하는 이유:
- 사용자 경험 반영 안 함
- 접근성 개선에 도움 안 됨
- 리팩토링 시 테스트 깨지기 쉬움
쿼리 변형
모든 쿼리는 3가지 변형이 있음:
getBy (동기, 즉시 실패)
screen.getByRole('button'); // 없으면 즉시 에러사용 시기: 요소가 이미 렌더링되어 있을 때
queryBy (동기, null 반환)
screen.queryByRole('button'); // 없으면 null 반환사용 시기: 요소가 없음을 검증할 때
expect(screen.queryByRole('alert')).not.toBeInTheDocument();findBy (비동기, Promise)
await screen.findByRole('button'); // 나타날 때까지 대기사용 시기: 비동기로 나타나는 요소 (API 호출, 애니메이션 후 등)
복수 요소 쿼리
getAllBy, queryAllBy, findAllBy
// 모든 버튼
const buttons = screen.getAllByRole('button');
expect(buttons).toHaveLength(3);
// 모든 리스트 아이템
const items = screen.getAllByRole('listitem');
expect(items[0]).toHaveTextContent('첫 번째');
// 비동기로 나타나는 여러 요소
const notifications = await screen.findAllByRole('alert');실전 예시
폼 테스트
test('사용자가 회원가입 폼을 작성할 수 있다', async () => {
const user = userEvent.setup();
render(<SignupForm />);
// 1순위: getByRole
await user.type(
screen.getByRole('textbox', { name: /이메일/i }),
'user@example.com'
);
// 1순위: getByRole (password는 role 없으므로 getByLabelText 사용)
await user.type(screen.getByLabelText(/비밀번호/i), 'password123');
// 1순위: getByRole
await user.click(screen.getByRole('checkbox', { name: /약관 동의/i }));
// 1순위: getByRole
await user.click(screen.getByRole('button', { name: /가입하기/i }));
// 3순위: findBy (비동기 결과)
expect(await screen.findByText(/가입이 완료되었습니다/i)).toBeInTheDocument();
});네비게이션 테스트
test('네비게이션 메뉴가 올바르게 동작한다', async () => {
const user = userEvent.setup();
render(<Navigation />);
// 1순위: getByRole (navigation)
const nav = screen.getByRole('navigation');
// 1순위: getByRole (link)
const homeLink = within(nav).getByRole('link', { name: /홈/i });
const aboutLink = within(nav).getByRole('link', { name: /소개/i });
await user.click(aboutLink);
// 1순위: getByRole (heading)
expect(screen.getByRole('heading', { name: /소개/i })).toBeInTheDocument();
});모달 테스트
test('모달을 열고 닫을 수 있다', async () => {
const user = userEvent.setup();
render(<App />);
// 모달 없음 확인 (queryBy)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
// 열기 버튼
await user.click(screen.getByRole('button', { name: /설정 열기/i }));
// 모달 나타남 (findBy 또는 getBy)
const dialog = await screen.findByRole('dialog');
expect(dialog).toBeVisible();
// 닫기 버튼 (within 사용)
await user.click(within(dialog).getByRole('button', { name: /닫기/i }));
// 모달 사라짐 확인
await waitFor(() => {
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});요약
쿼리 우선순위 체크리스트: 1. ✅ getByRole로 찾을 수 있는가? → 사용 2. ✅ 폼 요소 + label 있는가? → getByLabelText 고려 3. ✅ 일반 텍스트 콘텐츠인가? → getByText 4. ✅ 이미지 alt인가? → getByAltText (또는 getByRole('img')) 5. ⚠️ 다른 방법 모두 불가능한가? → getByTestId (최후 수단)
비동기 처리:
- 요소가 이미 있음 →
getBy - 요소가 없음을 검증 →
queryBy - 요소가 비동기로 나타남 →
findBy
User Events Reference
@testing-library/user-event API 전체 가이드. 실제 사용자 행동을 시뮬레이션하는 권장 방법.
기본 설정
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
test('example', async () => {
const user = userEvent.setup();
render(<MyComponent />);
// 모든 user 메서드는 await 필수
await user.click(screen.getByRole('button'));
});핵심:
userEvent.setup()호출 필수- 모든 메서드는
Promise반환 →await사용
마우스 상호작용
click
await user.click(element);
await user.click(element, { skipHover: true }); // hover 건너뛰기시뮬레이션 순서: 1. mouseover 2. mouseenter 3. mousemove 4. mousedown 5. focus (focusable 요소) 6. mouseup 7. click
dblClick (더블클릭)
await user.dblClick(element);tripleClick (트리플클릭 - 텍스트 선택)
await user.tripleClick(element);hover
await user.hover(element);
await user.unhover(element);pointer (고급 마우스 제어)
// 우클릭
await user.pointer({ keys: '[MouseRight]', target: element });
// 마우스 이동
await user.pointer({ coords: { x: 100, y: 200 } });
// 드래그 앤 드롭
await user.pointer([
{ keys: '[MouseLeft>]', target: dragElement },
{ coords: { x: 100, y: 200 } },
{ keys: '[/MouseLeft]' },
]);키보드 상호작용
type (텍스트 입력)
await user.type(element, 'Hello World');
// 특수 키 조합
await user.type(element, '{Shift}hello{/Shift}'); // "HELLO"
await user.type(element, 'foo{Backspace}{Backspace}'); // "f"
// delay 옵션 (기본: 0ms)
await user.type(element, 'slow typing', { delay: 100 });
// skipClick (초기 클릭 건너뛰기)
await user.type(element, 'text', { skipClick: true });특수 키 목록:
{Enter}- 엔터{Backspace}- 백스페이스{Delete}- 삭제{Escape}- ESC{Space}- 스페이스{Tab}- 탭{Shift}text{/Shift}- Shift 누른 상태로 입력{Control}a{/Control}- Ctrl+A{Alt}- Alt{Meta}- Command (Mac) / Windows (Win)
keyboard (키보드 직접 제어)
// 단일 키
await user.keyboard('a');
// 특수 키
await user.keyboard('{Enter}');
await user.keyboard('{Escape}');
// 키 조합
await user.keyboard('{Control>}a{/Control}'); // Ctrl+A (전체 선택)
await user.keyboard('{Meta>}c{/Meta}'); // Cmd+C (복사)
// 여러 키 순차 입력
await user.keyboard('hello{Enter}world');키 유지 문법:
{Shift>}- Shift 누르기 시작{/Shift}- Shift 떼기{Shift>}text{/Shift}- Shift 누른 상태로 "text" 입력
clear (입력값 지우기)
await user.clear(element);동작: 1. 요소 클릭 2. Ctrl+A (전체 선택) 3. Backspace
폼 상호작용
selectOptions (select 요소)
// <select>
// <option value="1">옵션 1</option>
// <option value="2">옵션 2</option>
// </select>
// value로 선택
await user.selectOptions(screen.getByRole('combobox'), '1');
// 텍스트로 선택
await user.selectOptions(screen.getByRole('combobox'), '옵션 2');
// 복수 선택 (multiple select)
await user.selectOptions(screen.getByRole('listbox'), ['1', '2']);deselectOptions (다중 선택 해제)
await user.deselectOptions(screen.getByRole('listbox'), '1');upload (파일 업로드)
const file = new File(['hello'], 'hello.png', { type: 'image/png' });
await user.upload(
screen.getByLabelText(/파일 선택/i),
file
);
// 복수 파일
await user.upload(input, [file1, file2]);검증:
const input = screen.getByLabelText(/파일 선택/i) as HTMLInputElement;
expect(input.files).toHaveLength(1);
expect(input.files?.[0]).toBe(file);클립보드
copy, cut, paste
// 복사
await user.copy();
// 잘라내기
await user.cut();
// 붙여넣기
await user.paste('pasted text');예시:
await user.type(screen.getByRole('textbox'), 'Hello');
await user.keyboard('{Control>}a{/Control}'); // 전체 선택
await user.copy();
const otherInput = screen.getByRole('textbox', { name: /다른 입력/i });
await user.click(otherInput);
await user.paste(); // "Hello" 붙여넣기탭 이동
tab
// 다음 요소로 tab
await user.tab();
// 이전 요소로 tab (Shift+Tab)
await user.tab({ shift: true });예시:
render(
<>
<input aria-label="첫 번째" />
<input aria-label="두 번째" />
<button>제출</button>
</>
);
const first = screen.getByLabelText(/첫 번째/i);
first.focus();
await user.tab(); // 두 번째 input으로 이동
expect(screen.getByLabelText(/두 번째/i)).toHaveFocus();
await user.tab(); // 버튼으로 이동
expect(screen.getByRole('button')).toHaveFocus();
await user.tab({ shift: true }); // 다시 두 번째 input으로
expect(screen.getByLabelText(/두 번째/i)).toHaveFocus();userEvent vs fireEvent
userEvent 사용 (권장)
const user = userEvent.setup();
// ✅ 실제 사용자 행동 시뮬레이션
await user.click(button);
await user.type(input, 'text');장점:
- 실제 브라우저 이벤트 순서 재현
- focus, blur, hover 자동 처리
- 접근성 검증 (disabled 요소 클릭 불가 등)
fireEvent 지양
// ❌ 저수준 이벤트만 발생
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'text' } });문제점:
- 실제 사용자 행동과 다름
- focus, hover 등 자동 처리 안 됨
- 접근성 검증 없음
예외적으로 fireEvent 사용해야 하는 경우:
- userEvent가 지원하지 않는 특수 이벤트 (예:
scroll,resize)
// scroll 이벤트
fireEvent.scroll(window, { target: { scrollY: 100 } });실전 예시
로그인 폼
test('사용자가 로그인할 수 있다', async () => {
const user = userEvent.setup();
render(<LoginForm />);
await user.type(
screen.getByRole('textbox', { name: /이메일/i }),
'user@example.com'
);
await user.type(
screen.getByLabelText(/비밀번호/i),
'password123'
);
await user.click(screen.getByRole('button', { name: /로그인/i }));
expect(await screen.findByText(/환영합니다/i)).toBeInTheDocument();
});검색 기능
test('사용자가 검색할 수 있다', async () => {
const user = userEvent.setup();
render(<SearchBar />);
const searchInput = screen.getByRole('searchbox');
await user.type(searchInput, 'React');
await user.keyboard('{Enter}');
expect(await screen.findByText(/검색 결과/i)).toBeInTheDocument();
});체크박스 토글
test('사용자가 체크박스를 토글할 수 있다', async () => {
const user = userEvent.setup();
render(<TodoItem />);
const checkbox = screen.getByRole('checkbox', { name: /완료 표시/i });
expect(checkbox).not.toBeChecked();
await user.click(checkbox);
expect(checkbox).toBeChecked();
await user.click(checkbox);
expect(checkbox).not.toBeChecked();
});드롭다운 선택
test('사용자가 국가를 선택할 수 있다', async () => {
const user = userEvent.setup();
render(<CountrySelector />);
const select = screen.getByRole('combobox', { name: /국가/i });
await user.selectOptions(select, '한국');
expect(select).toHaveValue('kr');
expect(screen.getByRole('option', { name: /한국/i })).toBeInTheDocument();
});파일 업로드
test('사용자가 파일을 업로드할 수 있다', async () => {
const user = userEvent.setup();
render(<FileUploader />);
const file = new File(['content'], 'example.txt', { type: 'text/plain' });
const input = screen.getByLabelText(/파일 선택/i);
await user.upload(input, file);
expect(await screen.findByText(/example.txt/i)).toBeInTheDocument();
});키보드 단축키
test('Ctrl+S로 저장할 수 있다', async () => {
const user = userEvent.setup();
const onSave = vi.fn();
render(<Editor onSave={onSave} />);
const textarea = screen.getByRole('textbox');
await user.type(textarea, 'Some content');
await user.keyboard('{Control>}s{/Control}');
expect(onSave).toHaveBeenCalledWith('Some content');
});탭 네비게이션
test('Tab으로 폼 요소 간 이동이 가능하다', async () => {
const user = userEvent.setup();
render(<ContactForm />);
const nameInput = screen.getByRole('textbox', { name: /이름/i });
const emailInput = screen.getByRole('textbox', { name: /이메일/i });
const submitButton = screen.getByRole('button', { name: /제출/i });
nameInput.focus();
expect(nameInput).toHaveFocus();
await user.tab();
expect(emailInput).toHaveFocus();
await user.tab();
expect(submitButton).toHaveFocus();
});복사/붙여넣기
test('텍스트를 복사하여 다른 입력창에 붙여넣을 수 있다', async () => {
const user = userEvent.setup();
render(
<>
<input aria-label="원본" defaultValue="Hello World" />
<input aria-label="대상" />
</>
);
const source = screen.getByLabelText(/원본/i);
const target = screen.getByLabelText(/대상/i);
// 원본 텍스트 전체 선택 및 복사
await user.click(source);
await user.keyboard('{Control>}a{/Control}');
await user.copy();
// 대상에 붙여넣기
await user.click(target);
await user.paste();
expect(target).toHaveValue('Hello World');
});옵션 및 설정
setup 옵션
const user = userEvent.setup({
// 이벤트 간 기본 지연 (ms)
delay: null,
// 문서 객체 (기본: global.document)
document: customDocument,
// 포인터 맵
pointerMap: customPointerMap,
// 포인터 이벤트 비활성화
pointerEventsCheck: 0,
// Clipboard API 사용 여부
writeToClipboard: false,
});delay 옵션
// 모든 이벤트에 100ms 지연
const user = userEvent.setup({ delay: 100 });
// 특정 메서드만 지연
await user.type(input, 'text', { delay: 50 });사용 사례:
- 디바운스/쓰로틀 로직 테스트
- 애니메이션 중 상호작용 테스트
요약
기본 패턴:
const user = userEvent.setup();
await user.[method](element, ...args);자주 사용하는 메서드:
click- 클릭type- 텍스트 입력keyboard- 키보드 직접 제어selectOptions- 드롭다운 선택upload- 파일 업로드tab- 탭 이동
핵심 원칙: 1. 항상 userEvent.setup() 호출 2. 모든 메서드에 await 사용 3. fireEvent 대신 userEvent 사용 4. 실제 사용자 행동 시뮬레이션
Related skills
FAQ
What test layers does the testing skill cover?
The testing skill covers unit, integration, and regression test layers in dalestudy/skills codebases. Developers use it to author cases, organize suites, and run validation before release.
When should developers invoke the testing skill?
Developers invoke testing when a dalestudy/skills project needs new test coverage, better suite structure, or a pre-release regression run to catch defects before merge or deployment.