Test
[Jest] React Testing Library 사용하기
jalalja
2024. 12. 17. 18:07
React Testing Library란?
더보기
- 사용자 중심 테스트 지향
- RTL은 컴포넌트의 구현 방식보다는 UI 요소를 렌더링하고, 사용자가 상호작용하는 방식에 중점을 둬서 테스트를 진행한다.
- 즉, 버튼 클릭, 텍스트 입력, 요소의 표시 여부 등을 테스트할 때 사용한다.
- DOM 상호작용 테스트
- RTL은 querySelector와 같은 DOM 메서드를 사용하여 컴포넌트 내의 요소를 선택하고 상호작용할 수 있게 해준다.
- 예를 들어, getByText, getByRole, getByLabelText 등 다양한 쿼리 메서드를 제공하여, 실제 화면에서 표시되는 텍스트나 역할 기반으로 요소를 찾을 수 있다.
- 어울리는 기술 스택
- RTL은 React 컴포넌트의 사용자 상호작용을 테스트하는 도구이고, Jest는 테스트를 실행하고 결과를 처리하는 역할을 한다.
- 주로 두가지 기술이 같이 쓰여진다.
React Testing Library 설치
더보기
- RTL을 사용하기 위해 설치해야하는 모듈
- Vite로 생성한 프로젝트일 경우 @types/react와 @types/react-dom은 설치가 되어있어서 별도로 해주지 않아도 된다.
yarn add -D @testing-library/react @testing-library/dom @testing-library/jest-dom @types/react @types/react-dom
Query 종류
더보기

- Query 란?
- 렌더링된 DOM에서 요소를 찾는 데 사용되는 도구들을 의미하는것으로, HTML 요소를 검색하고 선택하기 위해 사용되는 다양한 함수들이다.
- Query 유형 (DOM 요소를 찾는 방법)
- getBy
- 요소를 찾을 수 있으면 반환한다.
- 요소를 찾을 수 없거나, 1개를 초과하는 요소가 있을 경우 에러를 발생시킨다.
- 비동기 작업을 지원하지 않는다.
- 주로 요소가 항상 DOM에 존재할 때 사용한다.
- queryBy
- 요소를 찾을 수 있으면 반환한다.
- 요소를 찾을 수 없으면 null을 반환한다.
- 1개를 초과하는 요소가 있을 경우 에러를 발생시킨다.
- 비동기 작업을 지원하지 않는다.
- 주로 요소가 존재하지 않거나, 에러를 던지지 않는 방식으로 검사가 필요할 때 사용한다.
- findBy
- 요소를 찾을 수 있으면 반환한다.
- 요소를 찾을 수 없거나, 1개를 초과하는 요소가 있을 경우 에러를 발생시킨다.
- 요소가 즉시 존재하지 않더라도 retry를 통해 나타날 때까지 기다린다.
- 비동기 작업을 지원한다.
- 주로 비동기적으로 렌더링되는 요소를 테스트하거나, 요소가 나중에 DOM에 추가될 것을 예상하는 경우 사용한다.
- getAllBy
- 요소를 찾을 수 있으면 배열 형태로 반환한다.
- 요소를 찾을 수 없으면 에러를 발생시킨다.
- getBy와 동일하지만, 여러개의 요소를 선택할 때 사용한다.
- queryAllBy
- 요소를 찾을 수 있으면 배열 형태로 반환한다.
- 요소를 찾을 수 없으면 빈 배열을 반환한다.
- queryBy와 동일하지만, 여러개의 요소를 선택할 때 사용한다.
- findAllBy
- 요소를 찾을 수 있으면 배열 형태로 반환한다.
- 요소를 찾을 수 없으면 에러를 발생시킨다.
- findBy와 동일하지만, 여러개의 요소를 선택할 때 사용한다.

- 대표적인 Query 종류
- 전체 Query에 해당
- screen.getByRole()과 같은 방식으로 사용하면 container: HTMLElement 부분을 별도로 작성해주지 않아도 된다.
- ByRole (가장 권장되는 방법)
- 접근성 속성을 기반으로 DOM 요소를 찾는 가장 권장되는 RTL 쿼리이다.
- ex) <button>Submit</button> (role: button)
getByRole(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
role: string,
options?: {
hidden?: boolean = false,
name?: TextMatch,
description?: TextMatch,
selected?: boolean,
busy?: boolean,
checked?: boolean,
pressed?: boolean,
suggest?: boolean,
current?: boolean | string,
expanded?: boolean,
queryFallbacks?: boolean,
level?: number,
value?: {
min?: number,
max?: number,
now?: number,
text?: TextMatch,
}
}): HTMLElement
- ByLabelText
- 요소의 label과 연결된 텍스트(label과 input 관계)로 요소를 찾는다.
- ex) <label for="name">Name</label><input id="name" />
getByLabelText(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
text: TextMatch,
options?: {
selector?: string = '*',
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- ByPlaceholderText
- 요소의 placeholder 속성을 기준으로 요소를 찾는다.
- ex) <input placeholder="Enter your name" />
getByPlaceholderText(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- ByText
- 요소의 텍스트 콘텐츠(보이는 텍스트)를 기준으로 요소를 찾는다.
- ex) <button>Submit</button>
getByText(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
text: TextMatch,
options?: {
selector?: string = '*',
exact?: boolean = true,
ignore?: string|boolean = 'script, style',
normalizer?: NormalizerFn,
}): HTMLElement
- ByDisplayValue
- <input>이나 <textarea>의 value 속성 값을 기준으로 요소를 찾는다.
- ex) <input value="Daniel" />
getByDisplayValue(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
value: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- ByAltText
- 이미지의 alt 속성을 기준으로 요소를 찾는다.
- ex) <img alt="profile picture" />
getByAltText(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- ByTitle
- 요소의 title 속성을 기준으로 요소를 찾는다.
- ex) <div title="tooltip" />
getByTitle(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
title: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- ByTestId
- 요소의 data-testid 속성을 기준으로 요소를 찾는다.
- ex) <div data-testid="custom-element" />
getByTestId(
// If you're using `screen`, then skip the container argument:
container: HTMLElement,
text: TextMatch,
options?: {
exact?: boolean = true,
normalizer?: NormalizerFn,
}): HTMLElement
- CheatSheet
- 전반적인 테스트 방법이 적혀있다. (공식문서를 다 보기 힘든 경우 참고하기 좋다)
Cheatsheet | Testing Library
Get the printable cheat sheet
testing-library.com
RTL을 쓰기 전의 코드 바꿔보기 (Query 사용)
더보기
- App.tsx
import { FormEvent, useState } from "react";
import "./App.css";
function App() {
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [loginError, setLoginError] = useState<boolean>(false);
const onSubmit = (e: FormEvent) => {
e.preventDefault();
if (!email || !password) {
setLoginError(true);
return;
}
};
return (
<>
<header>
<h1>HEADER</h1>
</header>
{loginError && <p id="errorMessage">ERROR</p>}
<main>
<form onSubmit={onSubmit}>
<div>
<h2>email</h2>
<input
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<h2>password</h2>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button id="submitButton" type="submit">
Form 제출
</button>
</form>
</main>
<footer>
<h1>FOOTER</h1>
</footer>
</>
);
}
export default App;
- RTL을 사용하지 않은 테스트 코드
import { test } from "@jest/globals";
import { act } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
test("로그인 화면이 잘 보인다.", async () => {
// 테스트를 위한 #root와 동일한 요소 생성
const container = document.createElement("div");
// 해당 요소를 body 하단에 삽입
document.body.appendChild(container);
// container 안에 App 컴포넌트 렌더링
await act(() => {
createRoot(container).render(<App />);
});
// header안의 h1 태그가 가지고 있는 텍스트가 "HEADER"인지 테스트
expect(container.querySelector("header h1")?.textContent).toBe("HEADER");
});
test("이메일, 비밀번호 미입력시 에러메시지가 나타난다.", async () => {
// 테스트를 위한 #root와 동일한 요소 생성
const container = document.createElement("div");
// 해당 요소를 body 하단에 삽입
document.body.appendChild(container);
// container 안에 App 컴포넌트 렌더링
await act(() => {
createRoot(container).render(<App />);
});
// form을 제출할 버튼 가져오기
const submitButton = container.querySelector("#submitButton");
// 에러 메시지 가져오기
let errorMessage = container.querySelector("#errorMessage");
// form을 제출하기 전에는 에러 메시지가 없기때문에 undefined여야 한다.
expect(errorMessage?.textContent).toBeUndefined();
// form 제출 버튼 클릭 이벤트
await act(() => {
submitButton?.dispatchEvent(new MouseEvent("click"));
});
// form을 제출한 다음에 변한 에러 메시지를 가져온다.
errorMessage = container.querySelector("#errorMessage");
// 에러 메시지는 ERROR여야 한다.
expect(errorMessage?.textContent).toBe("ERROR");
});
- RTL을 사용한 코드
- 코드를 간단하고 가독성있게 작성할 수 있다.
import { test } from "@jest/globals";
import { fireEvent, render, screen } from "@testing-library/react";
import App from "../App";
import "@testing-library/jest-dom";
test("로그인 화면이 잘 보인다.", () => {
render(<App />);
expect(screen.getByText("HEADER")).toBeInTheDocument();
});
test("이메일, 비밀번호 입력 없이 로그인 버튼을 클릭하면 에러메시지가 표시된다", async () => {
render(<App />);
fireEvent.click(screen.getByRole("button"));
expect(screen.getByText("ERROR")).toBeInTheDocument();
});
RenderHook 사용
- React에서 사용하는 Custom Hook을 테스트하기 위해 사용한다.
더보기
- Custom Hook 이란?
- Custom Hook은 React에서 재사용 가능한 기능을 만들기 위한 함수이다.
- 간단히 말해서, 여러 컴포넌트에서 자주 사용하는 로직을 하나의 함수로 묶어서 재사용할 수 있도록 만들어 주는 도구이다.
- App.tsx 코드
import { FormEvent } from "react";
import "./App.css";
import useLogin from "./hooks/useLogin";
function App() {
const { values, error, handleChange, validate } = useLogin({
email: "",
password: "",
});
const onSubmit = (e: FormEvent) => {
e.preventDefault();
if (!validate()) {
return;
}
};
return (
<>
<header>
<h1>HEADER</h1>
</header>
{error && <p id="errorMessage">ERROR</p>}
<main>
<form onSubmit={onSubmit}>
<div>
<h2>email</h2>
<input
type="text"
name="email"
value={values.email}
onChange={handleChange}
/>
</div>
<div>
<h2>password</h2>
<input
type="password"
name="password"
value={values.password}
onChange={handleChange}
/>
</div>
<button id="submitButton" type="submit">
Form 제출
</button>
</form>
</main>
<footer>
<h1>FOOTER</h1>
</footer>
</>
);
}
export default App;
- 간단한 Custom Hook 코드
import { useState } from "react";
function useLogin(initialValues: { [key: string]: string }) {
const [values, setValues] = useState(initialValues);
const [error, setError] = useState(false);
// 사용자가 이메일과, 비밀번호를 입력하면 상태를 바꿔주는 함수
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setValues((prev) => ({ ...prev, [name]: value }));
};
// 이메일과 비밀번호가 비어있는지 확인하는 함수
const validate = () => {
const hasEmptyFields = Object.values(values).some((value) => !value);
setError(hasEmptyFields);
return hasEmptyFields;
};
return { values, error, handleChange, validate };
}
export default useLogin;
- 테스트 코드
import { renderHook, act } from "@testing-library/react";
import useLogin from "./useLogin";
describe("로그인 커스텀훅 테스트", () => {
test("초기값이 정상적으로 잘 저장이 되었는지 확인한다.", () => {
const { result } = renderHook(() => useLogin({ email: "", password: "" }));
// 이메일과 비밀번호는 빈값이고, 에러는 false여야한다.
expect(result.current.values.email).toBe("");
expect(result.current.values.password).toBe("");
expect(result.current.error).toBe(false);
});
test("사용자가 아이디 또는 비밀번호를 입력하면 값이 업데이트된다.", () => {
const { result } = renderHook(() => useLogin({ email: "", password: "" }));
// 이메일에 값을 넣어준다.
act(() => {
result.current.handleChange({
target: { name: "email", value: "ths_eksldpf@naver.com" },
} as React.ChangeEvent<HTMLInputElement>);
});
// 넣어준 값이 동일한지 확인한다.
expect(result.current.values.email).toBe("ths_eksldpf@naver.com");
});
test("아이디 또는 비밀번호가 빈 값일 경우 에러가 true로 바뀐다.", () => {
const { result } = renderHook(() => useLogin({ email: "", password: "" }));
// 이메일만 값을 넣어주고 비밀번호는 별도의 값을 넣어주지 않는다.
act(() => {
result.current.handleChange({
target: { name: "email", value: "ths_eksldpf@naver.com" },
} as React.ChangeEvent<HTMLInputElement>);
});
// validate 함수를 호출해서 error 상태를 업데이트 시켜준다.
act(() => {
result.current.validate();
});
// 비밀번호가 빈 값이기 때문에 에러 상태는 true 여야 한다.
expect(result.current.error).toBe(true);
});
});