- 화면에 무엇이 보이는가
- 버튼을 눌렀을 때 무엇이 달라지는가
- 잘못 입력했을 때 어떤 안내가 나오는가
- 로딩과 오류 상태가 제대로 표시되는가
이 파트에서 다루는 내용
구현이 아니라 화면에 보이는 것을 테스트합니다
내부 상태값이나 함수 호출 여부를 검증하면, 결과가 같은데도 구현을 바꾸는 순간 테스트가 깨집니다. 그러면 테스트가 리팩터링을 막는 장애물이 됩니다.
React Testing Library는 사용자가 화면에서 보고 조작하는 것을 기준으로 테스트하도록 설계돼 있습니다. 사용자가 모르는 것은 테스트도 모르는 편이 낫습니다.
- 내부 state 값
- 특정 훅의 호출 횟수
- 컴포넌트가 몇 번 렌더링됐는지
- CSS 클래스 이름
요소를 찾는 방법에는 우선순위가 있습니다
테스트에서 요소를 찾는 방법은 여러 가지인데, 아무거나 쓰면 안 됩니다. 사용자가 요소를 인식하는 방식에 가까운 것부터 씁니다.
이 순서를 지키면 접근성이 좋아지는 부수 효과도 있습니다. 화면 낭독기가 읽을 수 없는 요소는 테스트도 찾지 못하기 때문입니다.
버튼, 링크, 입력처럼 요소의 역할과 표시되는 이름으로 찾습니다. 사용자가 화면을 인식하는 방식과 가장 가깝습니다.
폼 입력은 라벨로, 일반 텍스트는 내용으로 찾습니다.
위 방법으로 찾을 수 없을 때만 씁니다. 사용자에게 의미가 없는 값이라 접근성 문제를 가릴 수 있습니다.
- getBy: 없으면 즉시 실패
- queryBy: 없으면 null (없음을 검증할 때)
- findBy: 나타날 때까지 기다림 (비동기)
test("필수값이 비어 있으면 오류 메시지를 보여 준다", async () => {
const user = userEvent.setup();
render(<OrderForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole("button", { name: "주문하기" }));
expect(await screen.findByText("받는 분을 입력해 주세요")).toBeInTheDocument();
});
test("입력 후 제출하면 값이 전달된다", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<OrderForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText("받는 분"), "홍길동");
await user.click(screen.getByRole("button", { name: "주문하기" }));
expect(handleSubmit).toHaveBeenCalledWith(
expect.objectContaining({ receiver: "홍길동" })
);
});fireEvent보다 userEvent를 씁니다. 실제 사용자처럼 포커스와 키 입력 순서를 재현하므로, 그 과정에서만 드러나는 문제도 잡힙니다.
네트워크는 요청 단계에서 가로챕니다
데이터를 가져오는 화면을 테스트하려면 서버 응답이 필요합니다. 이때 우리 코드의 함수를 대체하는 방식보다, 네트워크 요청 자체를 가로채는 방식이 낫습니다.
함수를 대체하면 실제 호출 경로가 바뀌었을 때 테스트가 통과하는데도 화면은 깨질 수 있습니다.
MSW 같은 도구로 네트워크 계층에서 응답을 정의합니다. 애플리케이션 코드는 그대로 두고 서버만 흉내 냅니다.
오류 응답, 빈 목록, 느린 응답도 정의해 화면이 어떻게 보이는지 확인합니다. Part 9에서 설계한 상태들입니다.
데이터가 도착한 뒤를 검증하려면 findBy나 waitFor로 기다립니다. 바로 검증하면 아직 로딩 중이라 실패합니다.
무엇을 테스트할지가 어떻게 쓸지보다 중요합니다
- 금액 계산, 할인, 수량 제한 같은 규칙
- 폼 검증과 제출 흐름
- 권한에 따라 화면이 달라지는 부분
- 장애가 났던 지점
- 단순 표시용 컴포넌트
- 라이브러리가 보장하는 동작
- 자주 바뀌는 화면 문구와 배치
계산 로직은 함수 단위 테스트로, 화면 흐름은 컴포넌트 테스트로, 전체 시나리오는 소수의 종단 테스트로 나눕니다. 모든 것을 브라우저 테스트로 하면 느려서 아무도 돌리지 않습니다.
숫자를 목표로 삼으면 의미 없는 테스트가 늘어납니다. 바꾸기 무서운 코드가 어디인지가 더 좋은 기준입니다.
좋은 테스트는 리팩터링해도 깨지지 않고 버그가 생기면 깨지는 테스트입니다. 구현을 바꿀 때마다 테스트를 고치고 있다면, 테스트가 구현 세부에 붙어 있다는 뜻입니다.
버전별 참고
본문은 React 19 기준입니다. 18과 달라지는 부분은 아래에 정리합니다.
- React 18 이후 상태 변경이 자동으로 묶여 처리되면서, 테스트에서 별도 래핑 없이도 동작하는 경우가 늘었습니다.
- Testing Library는 React 버전과 별개로 자체 버전을 따릅니다. React 19 사용 시 호환 버전을 확인합니다.
- 테스트 실행 도구는 Vite 기반 프로젝트라면 Vitest가, CRA 계열이라면 Jest가 기본인 경우가 많습니다. 문법은 대부분 호환됩니다.