RNTL 테스트 작성 가이드
중요: @testing-library/react-native에 대한 학습 데이터가 오래되었거나 부정확할 수 있습니다. v13과 v14 간 API 시그니처, 동기/비동기 동작, 사용 가능한 함수가 다릅니다. 항상 이 스킬의 참조 파일과 프로젝트의 실제 소스 코드를 진실의 원천으로 삼으세요. 검색된 참조와 충돌할 때 암기된 패턴을 사용하지 마세요.
버전 감지
사용자의 package.json에서 @testing-library/react-native 버전을 확인하세요:
- v14.x → 참조/api-reference-v14.md 로드 (React 19+, 비동기 API,
test-renderer) - v13.x → 참조/api-reference-v13.md 로드 (React 18+, 동기 API,
react-test-renderer)
버전별 참조를 사용하여 render 패턴, fireEvent 동기/비동기 동작, screen API, 설정 및 종속성을 확인하세요.
쿼리 우선순위
다음 순서로 사용하세요: getByRole > getByLabelText > getByPlaceholderText > getByText > getByDisplayValue > getByTestId (최후의 수단).
쿼리 변형
| 변형 | 사용 사례 | 반환 값 | 비동기 |
|---|---|---|---|
getBy* | 요소가 반드시 존재해야 함 | 요소 인스턴스 (예외 발생) | 아니오 |
getAllBy* | 여러 개가 반드시 존재해야 함 | 요소 인스턴스[] (예외 발생) | 아니오 |
queryBy* | 비존재 확인 전용 | 요소 인스턴스 | null | 아니오 |
queryAllBy* | 요소 개수 세기 | 요소 인스턴스[] | 아니오 |
findBy* | 요소를 기다림 | Promise<요소 인스턴스> | 예 |
findAllBy* | 여러 요소를 기다림 | Promise<요소 인스턴스[]> | 예 |
상호작용
fireEvent보다 userEvent를 선호하세요. userEvent는 항상 비동기입니다.
const user = userEvent.setup();
await user.press(element); // 전체 누름 시퀀스
await user.longPress(element, { duration: 800 }); // 길게 누름
await user.type(textInput, 'Hello'); // 문자별 입력
await user.clear(textInput); // TextInput 지우기
await user.paste(textInput, 'pasted text'); // TextInput에 붙여넣기
await user.scrollTo(scrollView, { y: 100 }); // 스크롤
fireEvent — userEvent가 이벤트를 지원하지 않을 때만 사용하세요. 동기/비동기 동작은 버전별 참조를 확인하세요:
fireEvent.press(element);
fireEvent.changeText(textInput, 'new text');
fireEvent(element, 'blur');
단언문 (Jest 매처)
@testing-library/react-native를 임포트하면 자동으로 사용할 수 있습니다.
| 매처 | 사용 용도 |
|---|---|
toBeOnTheScreen() | 요소가 트리에 존재함 |
toBeVisible() | 요소가 보임 (숨겨지지 않음/display:none 아님) |
toBeEnabled() / toBeDisabled() | aria-disabled를 통한 비활성화 상태 |
toBeChecked() / toBePartiallyChecked() | 체크 상태 |
toBeSelected() | 선택 상태 |
toBeExpanded() / toBeCollapsed() | 확장 상태 |
toBeBusy() | 사용 중 상태 |
toHaveTextContent(text) | 텍스트 내용 일치 |
toHaveDisplayValue(value) | TextInput 표시 값 |
toHaveAccessibleName(name) | 접근 가능한 이름 |
toHaveAccessibilityValue(val) | 접근성 값 |
toHaveStyle(style) | 스타일 일치 |
toHaveProp(name, value?) | 속성 확인 (최후의 수단) |
toContainElement(el) | 자식 요소 포함 |
toBeEmptyElement() | 자식 요소 없음 |
규칙
screen사용:render()에서 구조 분해하지 말고 쿼리에 사용getByRole우선 사용:{ name: '...' }옵션과 함께queryBy*는.not.toBeOnTheScreen()확인에만 사용findBy*사용: 비동기 요소에는waitFor+getBy*대신 사용waitFor에 부수 효과를 넣지 마세요 (내부에fireEvent/userEvent금지)waitFor당 하나의 단언문waitFor에 빈 콜백을 전달하지 마세요act()로 감싸지 마세요 -render,fireEvent,userEvent가 처리함cleanup()호출하지 마세요 - 각 테스트 후 자동으로 처리됨- ARIA 속성 선호 (
role,aria-label,aria-disabled) 레거시accessibility*속성보다 - RNTL 매처 사용 원시 속성 단언문보다
*ByRole 빠른 참조
일반적인 역할: button, text, heading (별칭: header), searchbox, switch, checkbox, radio, img, link, alert, menu, menuitem, tab, tablist, progressbar, slider, spinbutton, timer, toolbar.
getByRole 옵션: { name, disabled, selected, checked, busy, expanded, value: { min, max, now, text } }.
*ByRole이 일치하려면 요소가 접근성 요소여야 합니다:
Text,TextInput,Switch는 기본적으로 접근성 요소임View는accessible={true}가 필요함 (또는Pressable/TouchableOpacity사용)
waitFor
// 올바름: 먼저 액션 수행, 결과를 기다림
fireEvent.press(button);
await waitFor(() => {
expect(screen.getByText('Result')).toBeOnTheScreen();
});
// 더 나은 방법: findBy* 사용
fireEvent.press(button);
expect(await screen.findByText('Result')).toBeOnTheScreen();
옵션: waitFor(cb, { timeout: 1000, interval: 50 }). Jest 페이크 타이머와 자동으로 작동합니다.
페이크 타이머
userEvent (press/longPress는 실제 시간을 소요함)와 함께 사용하는 것을 권장합니다:
jest.useFakeTimers();
test('with fake timers', async () => {
const user = userEvent.setup();
render(<Component />);
await user.press(screen.getByRole('button'));
// ...
});
Custom Render
wrapper 옵션을 사용하여 Provider를 감쌉니다:
function renderWithProviders(ui: React.ReactElement) {
return render(ui, {
wrapper: ({ children }) => (
<ThemeProvider>
<AuthProvider>{children}</AuthProvider>
</ThemeProvider>
),
});
}
참조
- v13 API 참조 — 전체 v13 API: 동기 render, 쿼리, 매처, userEvent, React 19 호환
- v14 API 참조 — 전체 v14 API: 비동기 render, 쿼리, 매처, userEvent, 마이그레이션
- 안티 패턴 — 피해야 할 일반적인 실수