~18 min · vitest-components, queries, accessibility, rtl
Level 0테스트 호기심
0 XP0/32 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"사용자가 보는 걸로 쿼리해. Accessibility 트리가 사용자 인터페이스야."
우선순위 리스트 (와 그 이유)
RTL 공식 쿼리 우선순위 — 다음으로 fallback 하기 전 시도해야 할 순서:
getByRole + { name } 옵션 — 골드 스탠다드.
getByLabelText — 적절한 라벨 있는 폼 필드용.
getByPlaceholderText — 라벨 없을 때만 (드뭄).
getByText — non-interactive 컨텐츠용.
getByDisplayValue — 채워진 폼 필드용.
getByAltText — 이미지용.
getByTitle — title 속성 가진 element 의 최후 수단.
getByTestId — 다른 게 element 식별 못 할 때 escape hatch.
이 리스트가 이 순서인 이유: 위쪽 쿼리들이 assistive tech (스크린 리더, 보이스 컨트롤) 가 element 찾는 방식을 거울처럼 반영, 그게 사람이 페이지 훑는 방식이기도 해. 이 쿼리 쓰는 테스트는 암묵적으로 페이지가 쓸 수 있는지 테스트. getByTestId 에 손 뻗는 테스트는 페이지가 스크린 리더에 못 쓰는데 통과 가능.
`getByRole` + `name` 이 일꾼
대부분 인터랙티브 element 가 암묵적 ARIA role 가져: <button> 은 button, <h2> 는 heading, <a href> 는 link, <input type="checkbox"> 는 checkbox. getByRole('button', { name: /save/i }) 가 접근 가능 이름이 /save/i 매칭되는 버튼 찾아 — resilience 위한 case-insensitive regex 매칭.
name 옵션은 순서대로 계산: aria-label, aria-labelledby 내용, element 텍스트 컨텐츠, 특정 속성 값 (이미지는 alt, 일부 element 는 title). 이름으로 쿼리하면 스크린 리더가 발음할 같은 문자열 쿼리하는 거.
Role 로 쿼리 못 하면 컴포넌트가 접근 가능하지 않을 수 있어. 클릭 가능한 <div onClick> 는 role 도 name 도 없어 — 스크린 리더가 못 찾고, 너의 테스트도 못 찾아. 컴포넌트 고쳐 (<button> 써), getByTestId 를 workaround 로 쓰지 마.
textbox — <input type="text|email|..."> 와 <textarea>
checkbox, radio, combobox (select), option
list — <ul>, <ol>
listitem — <li>
img — <img alt> (alt 없으면 role 없음)
navigation — <nav>
main — <main>
article — <article>
dialog — <dialog> 또는 role="dialog" 가진 모달
`getByText` 가 값어치할 때
getByText 는 non-interactive 컨텐츠 — 본문, 상태 메시지, 에러 텍스트 — 에 맞아. "비어있을 때 폼 제출하면 'Email is required' 보임" 은 자연스럽게 screen.getByText(/email is required/i). 텍스트가 매칭된다고 인터랙티브 element 에 쓰지 마. Submit 버튼은 여전히 getByRole('button', { name: /submit/i }) 가 이겨.
// ✅ Good — queries mirror how users find elements
import { render, screen } from '@testing-library/react';
import { LoginForm } from './login-form';
it('renders the form with the right controls', () => {
render(<LoginForm onSubmit={() => {}} />);
expect(screen.getByRole('textbox', { name: /email/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
// ❌ Anti-pattern — using test ids when role + name would work
it('renders the form (the brittle way)', () => {
// Now the test depends on data-testid attributes the user can't see.
// If a designer removes the data-testid, the test breaks but the UI works.
render(<LoginForm onSubmit={() => {}} />);
expect(screen.getByTestId('email-input')).toBeInTheDocument();
expect(screen.getByTestId('submit-button')).toBeInTheDocument();
});
쿼리 정제 — `level`, `name`, regex 유연성·tsx
// Refining queries with options
import { render, screen } from '@testing-library/react';
it('finds the right heading level', () => {
render(<ArticleWithMultipleHeadings />);
// Match the h2 specifically, not the h1 or h3
const sectionHeading = screen.getByRole('heading', {
name: /related articles/i,
level: 2,
});
expect(sectionHeading).toBeInTheDocument();
});
it('handles multiple buttons with disambiguation', () => {
render(<DeleteConfirmDialog />);
// The dialog has two buttons; the `name` option disambiguates.
expect(
screen.getByRole('button', { name: /confirm delete/i })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /cancel/i })
).toBeInTheDocument();
});
test-id — 정직한 escape hatch·tsx
// When test-id is genuinely the right answer
// (rare — usually for components that have no other accessible identifier)
import { render, screen } from '@testing-library/react';
it('finds a chart container', () => {
render(<RevenueChart data={...} />);
// Charts often render as <svg> with no inherent role or accessible name.
// A test-id is honest here: there is no user-visible name to query by.
expect(screen.getByTestId('revenue-chart')).toBeInTheDocument();
});
// Better, if you can add accessibility metadata:
// <svg role="img" aria-label="Revenue chart"> → getByRole('img', { name: /revenue chart/i })
위 코드 블록의 로그인 폼 가져와. 추가해: (1) 빈 이메일로 submit 하면 나타나는 에러 메시지, (2) 'Remember me' 체크박스, (3) 'Forgot password?' 링크. 그 다음 각각 쿼리하는 테스트 짜: 헤딩 (role: heading), 이메일 input (role: textbox, name), submit 버튼 (role: button, name), 에러 텍스트 (getByText), 체크박스 (role: checkbox, name), forgot 링크 (role: link, name). 아무것도 getByTestId 안 써.
Hint
test-id 없는 쿼리 못 찾으면 컴포넌트가 접근성 메타데이터 (라벨, aria-label, alt 텍스트) 빠진 거. 컴포넌트 먼저 고쳐 — 테스트는 자연스럽게 따라와.
Progress
Progress is local-only — sign in to sync across devices.