React Testing Library(RTL) ํบ์๋ณด๊ธฐ! ๐งช
“์ปดํฌ๋ํธ๊ฐ ์ฌ์ฉ๋๋ ๋ฐฉ์ ๊ทธ๋๋ก ํ ์คํธํ๋ผ!”
React Testing Library(RTL)์ ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธ๋ฅผ ์์ฑํ ์ ์๋๋ก ๋๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๋๋ค.
๋จ์ํ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๋์ง ํ์ธํ๋ ๊ฒ์ด ์๋๋ผ, ์ฌ์ฉ์์ ํ๋์ ๊ณ ๋ คํ์ฌ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ์ด ๋ชฉํ์ ๋๋ค.
1๏ธโฃ React Testing Library(RTL)์ด๋?
๐ก RTL์ ๋จ์ํ UI์ DOM ์์๋ฅผ ํ์ธํ๋ ๊ฒ์ด ์๋๋ผ,
“์ฌ์ฉ์๊ฐ ์น์ ์ค์ ๋ก ์ฌ์ฉํ ๋์ฒ๋ผ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฒ”์ ๋ชฉํ๋ก ํฉ๋๋ค.
๐ก๊ธฐ์กด์ Enzyme๊ณผ ๋น๊ตํ๋ฉด?
- โ Enzyme: ์ปดํฌ๋ํธ์ ๋ด๋ถ ๊ตฌํ์ ์ค์ ์ ์ผ๋ก ํ ์คํธ
- โ RTL: ์ฌ์ฉ์์ ํ๋์ ๊ธฐ๋ฐ์ผ๋ก ํ ์คํธ
โ Jest vs RTL ๋น๊ต
ํญ๋ชฉ | Jest | RTL |
๋ชฉ์ | ํ ์คํธ ํ๋ ์์ํฌ (์คํ ํ๊ฒฝ) | React ์ปดํฌ๋ํธ ํ ์คํธ |
๋ ๋๋ง | ์์ | render() API ์ ๊ณต |
๋น๋๊ธฐ ์ง์ | async/await ์ฌ์ฉ | findBy, waitFor ์ง์ |
์ ์ ์ด๋ฒคํธ | fireEvent ์ฌ์ฉ | userEvent ์ง์ (๋ ์ง๊ด์ ) |
์ค๋ ์ท ํ ์คํธ | ์ง์ | Jest์ ํจ๊ป ์ฌ์ฉ ๊ฐ๋ฅ |
โ ์ฆ, Jest๋ ํ ์คํธ๋ฅผ ์คํํ๋ ํ๊ฒฝ์ด๊ณ , RTL์ React ์ปดํฌ๋ํธ๋ฅผ ํ ์คํธํ๊ธฐ ์ํ ๋๊ตฌ
โ Jest๋ ์คํ ๋๊ตฌ (ํ ์คํธ๋ฅผ ์คํ)
โ RTL์ ์ปดํฌ๋ํธ๋ฅผ ์ค์ ์ฌ์ฉ์์ ๊ด์ ์์ ํ ์คํธํ๋ ๋๊ตฌ (UI ๊ฒ์ฆ)
2๏ธโฃ RTL์ ์ฒ ํ: “์ฌ์ฉ์ ๊ฒฝํ์ ์ค์ฌ์ผ๋ก ํ ์คํธํ๋ผ!”
โ “React Testing Library๋ ๊ตฌํ ์ธ๋ถ ์ฌํญ์ด ์๋๋ผ, ์ฌ์ฉ์์ ์ ์ฅ์์ ํ ์คํธํ๋ ๊ฒ์ด ์ค์ํ๋ค.”
๐ก ๊ธฐ์กด ํ ์คํธ ๋ฐฉ์ (๊ตฌํ ์ธ๋ถ ์ฌํญ ํ ์คํธ)
const { getByTestId } = render(<Counter />);
expect(getByTestId("counter-value").textContent).toBe("0");
โ data-testid๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์คํธ → ์ค์ ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ๋๋จ์ด์ง ๋ฐฉ์
๐ก ๊ถ์ฅ๋๋ RTL ํ ์คํธ ๋ฐฉ์
const { getByRole } = render(<Counter />);
expect(getByRole("heading", { level: 1 })).toHaveTextContent("0");
โ ์ ๊ทผ์ฑ ์์ฑ(A11y)์ ๊ธฐ๋ฐ์ผ๋ก ์์๋ฅผ ์ฐพ๊ธฐ ๋๋ฌธ์, ์ค์ ์ฌ์ฉ์์ ๋์ผํ ๋ฐฉ์์ผ๋ก ํ ์คํธ ๊ฐ๋ฅ
โ ์ฆ, HTML ์์์ ๊ตฌํ ์ธ๋ถ ์ฌํญ์ด ์๋๋ผ,
์ค์ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ ๋ฐฉ์์ผ๋ก ํ ์คํธ๋ฅผ ์์ฑํด์ผ ํ๋ค!
๐ RTL์ด ํ์ํ ์ด์ ?
- โ ์ฌ์ฉ์ ์ค์ฌ ํ ์คํธ → ๋ฒํผ ํด๋ฆญ, ์ ๋ ฅ ๋ฑ ์ค์ ์ฌ์ฉ์์ ์ ์ฌํ ํ ์คํธ ๊ฐ๋ฅ
- โ ์ปดํฌ๋ํธ์ ๋ด๋ถ ๊ตฌํ๋ณด๋ค ์ค์ ๋์ ๊ฒ์ฆ
- โ Jest์ ์๋ฒฝํ ํธํ
- โ ๋ ๋ฆฝ์ ์ธ ํ ์คํธ ํ๊ฒฝ ์ ๊ณต (DOM ์กฐ์ ์์ด ๊ฐ์ ํ๊ฒฝ์์ ์คํ)
3๏ธโฃ React Testing Library ์ค์น ๋ฐ ์ค์
๐ง ์ค์น ๋ฐฉ๋ฒ
๋จผ์ , ํ๋ก์ ํธ์ React Testing Library๋ฅผ ์ถ๊ฐํฉ๋๋ค.
npm install --save-dev @testing-library/react @testing-library/jest-dom
- @testing-library/react: React ์ปดํฌ๋ํธ๋ฅผ ํ ์คํธํ ์ ์๋๋ก ์ง์
- @testing-library/jest-dom: Jest์ ํจ๊ป ์ฌ์ฉํด DOM ํ ์คํธ๋ฅผ ์ฝ๊ฒ ํจ
๐ Jest ์ค์ (package.json)
"scripts": {
"test": "react-scripts test"
}
Jest๋ ๊ธฐ๋ณธ์ ์ผ๋ก react-scripts์ ํฌํจ๋์ด ์์ผ๋ฏ๋ก, ๋ณ๋์ ์ค์น ์์ด ์ฌ์ฉํ ์ ์์ต๋๋ค.
4๏ธโฃ React Testing Library ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๐ ๊ฐ๋จํ ํ ์คํธ ์์ฑ
๊ธฐ๋ณธ์ ์ธ React ์ปดํฌ๋ํธ ํ ์คํธ ์์ ๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
๐ ์์ : ๋ฒํผ ํด๋ฆญ ํ ์คํธ
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import Counter from "../Counter"; // ํ
์คํธํ ์ปดํฌ๋ํธ
test("๋ฒํผ์ ํด๋ฆญํ๋ฉด ์นด์ดํธ๊ฐ ์ฆ๊ฐํด์ผ ํ๋ค", () => {
render(<Counter />);
const button = screen.getByRole("button", { name: "์ฆ๊ฐ" });
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(screen.getByText("์นด์ดํธ: 1")).toBeInTheDocument();
});
๐ ์ฃผ์ ํจ์ ๋ฐ API ์ค๋ช
ํจ์ | ์ค๋ช |
render(<Component />) | ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ DOM์ ๋ ๋๋ง |
screen.getByText(text) | ํน์ ํ ์คํธ๋ฅผ ๊ฐ์ง ์์ ์ฐพ๊ธฐ |
screen.getByRole(role, options) | ์ญํ ๊ธฐ๋ฐ ์์ ์ฐพ๊ธฐ (button, heading ๋ฑ) |
fireEvent.click(element) | ํด๋ฆญ ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ |
expect(element).toBeInTheDocument() | ์์๊ฐ ์กด์ฌํ๋์ง ํ์ธ |
5๏ธโฃ RTL ํต์ฌ API & ์ฃผ์ ๋ฉ์๋
๐ render() - ์ปดํฌ๋ํธ ๋ ๋๋ง
import { render } from "@testing-library/react";
import Counter from "./Counter";
test("Counter๊ฐ ์ ์์ ์ผ๋ก ๋ ๋๋ง๋๋์ง ํ์ธ", () => {
render(<Counter />);
});
โ render()๋ฅผ ํตํด React ์ปดํฌ๋ํธ๋ฅผ ์ค์ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์ฒ๋ผ ๋ ๋๋ง
๐ screen - DOM ์์ ์ฐพ๊ธฐ
import { render, screen } from "@testing-library/react";
import Counter from "./Counter";
test("๋ฒํผ์ด ์กด์ฌํ๋์ง ํ์ธ", () => {
render(<Counter />);
expect(screen.getByText("์ฆ๊ฐ")).toBeInTheDocument();
});
โ screen.getByText("์ฆ๊ฐ") → ์ค์ ์ฌ์ฉ์์ฒ๋ผ ํ ์คํธ๋ก ์์๋ฅผ ์ฐพ๋ ๋ฐฉ์
6๏ธโฃ ์ด๋ฒคํธ ํ ์คํธ (์ ์ ์ํธ์์ฉ)
๐ userEvent vs fireEvent ์ฐจ์ด์
๋ฉ์๋ | ์ค๋ช | ์์ |
fireEvent | ๊ธฐ๋ณธ DOM ์ด๋ฒคํธ ํธ๋ฆฌ๊ฑฐ | fireEvent.click(button) |
userEvent | ์ค์ ์ฌ์ฉ์ ์ด๋ฒคํธ ์๋ฎฌ๋ ์ด์ | userEvent.click(button) |
๐ก ์ฌ์ฉ์ ์ํธ์์ฉ ํ ์คํธ ์์
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
test("๋ฒํผ ํด๋ฆญ ์ ์นด์ดํธ ์ฆ๊ฐ", async () => {
render(<Counter />);
const button = screen.getByRole("button", { name: "์ฆ๊ฐ" });
await userEvent.click(button);
expect(screen.getByText("1")).toBeInTheDocument();
});
โ userEvent.click()์ ์ฌ์ฉํ๋ฉด ์ค์ ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ๋ ๊ฒ๊ณผ ์ ์ฌํ ๋ฐฉ์์ผ๋ก ๋์
7๏ธโฃ ๋น๋๊ธฐ ํ ์คํธ (findBy, waitFor)
โ ๋น๋๊ธฐ UI๊ฐ ๋ณ๊ฒฝ๋ ๋ ๊ธฐ๋ค๋ ค์ผ ํ๋ค๋ฉด findBy ๋๋ waitFor ์ฌ์ฉ
๐ก API ํธ์ถ ํ ๋ก๋ฉ ์คํผ๋๊ฐ ์ฌ๋ผ์ง๋์ง ํ์ธ
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
test("๋ฒํผ ํด๋ฆญ ํ ๋น๋๊ธฐ ๋ฐ์ดํฐ ํ์ธ", async () => {
render(<Counter />);
const button = screen.getByText("์ฆ๊ฐ");
userEvent.click(button);
expect(await screen.findByText("๋ฐ์ดํฐ ๋ก๋ฉ ์๋ฃ")).toBeInTheDocument();
});
โ findByText๋ ๋น๋๊ธฐ ๋ ๋๋ง์ด ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆผ
๐ก API ์๋ต์ด ๋๋ ํ UI๊ฐ ๋ณ๊ฒฝ๋๋์ง ํ์ธ
await waitFor(() => expect(screen.getByText("๋ฐ์ดํฐ ๋ก๋ฉ ์๋ฃ")).toBeInTheDocument());
โ waitFor()์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ๋ฒ UI ์ ๋ฐ์ดํธ๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ์๋ ์์ ์ ์ธ ํ ์คํธ ๊ฐ๋ฅ
8๏ธโฃ ํ ์คํธ ์์ฑ ๊ธฐ๋ฒ
1. ์ปดํฌ๋ํธ ํ ์คํธ
์ปดํฌ๋ํธ๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๋ ๋๋ง๋๋์ง ๊ฒ์ฆํฉ๋๋ค.
๐ ์์ : Input ์ปดํฌ๋ํธ ํ ์คํธ
import { render, screen } from "@testing-library/react";
import Input from "../Input";
test("์
๋ ฅ ํ๋๊ฐ ๋ ๋๋ง๋์ด์ผ ํ๋ค", () => {
render(<Input />);
expect(screen.getByPlaceholderText("์ด๋ฆ์ ์
๋ ฅํ์ธ์")).toBeInTheDocument();
});
2. ์ฌ์ฉ์ ์ํธ์์ฉ ํ ์คํธ
์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ๊ฑฐ๋ ์ ๋ ฅํ๋ ๋์์ ํ ์คํธํฉ๋๋ค.
import { render, screen, fireEvent } from "@testing-library/react";
import Input from "../Input";
test("์
๋ ฅ ํ๋์ ํ
์คํธ๋ฅผ ์
๋ ฅํ๋ฉด ๊ฐ์ด ๋ณ๊ฒฝ๋์ด์ผ ํ๋ค", () => {
render(<Input />);
const input = screen.getByPlaceholderText("์ด๋ฆ์ ์
๋ ฅํ์ธ์");
fireEvent.change(input, { target: { value: "Alice" } });
expect(input.value).toBe("Alice");
});
3. ๋น๋๊ธฐ ํ ์คํธ
API ํธ์ถ ํ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ํ ์คํธ์ ๋๋ค.
๐ ์์ : ๋น๋๊ธฐ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
import { render, screen, waitFor } from "@testing-library/react";
import UserList from "../UserList";
test("์ ์ ๋ชฉ๋ก์ ์ฌ๋ฐ๋ฅด๊ฒ ๋ถ๋ฌ์์ผ ํ๋ค", async () => {
render(<UserList />);
await waitFor(() => {
expect(screen.getByText("์ ์ ๋ชฉ๋ก")).toBeInTheDocument();
});
});
9๏ธโฃ ๋๋ฒ๊น ๋ฐ ํ ์คํธ ์คํจ ์ ๋์ฒ๋ฒ
โ ํ ์คํธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฉด, screen.debug()๋ฅผ ํ์ฉํ๋ฉด DOM ๊ตฌ์กฐ๋ฅผ ํ์ธ ๊ฐ๋ฅ
๐ก ํ ์คํธ ์คํจ ์ ๋๋ฒ๊น ์์
test("๋ฒํผ ํด๋ฆญ ํ ํ
์คํธ ๋ณ๊ฒฝ", async () => {
render(<Counter />);
screen.debug(); // ํ์ฌ DOM ์ํ ์ถ๋ ฅ
});
โ debug()๋ฅผ ์คํํ๋ฉด ํ์ฌ ๋ ๋๋ง๋ DOM์ ํฐ๋ฏธ๋์์ ํ์ธ ๊ฐ๋ฅ
7๏ธโฃ ์ค๋ ์ท ํ ์คํธ (Snapshot Testing)
โ ์ค๋ ์ท ํ ์คํธ๋ ๋ ๋๋ UI๊ฐ ๋ณ๊ฒฝ๋์ง ์์๋์ง ํ์ธํ๋ ๋ฐฉ๋ฒ
๐ก ๊ธฐ๋ณธ ์ค๋ ์ท ํ ์คํธ
import { render } from "@testing-library/react";
import Counter from "./Counter";
test("์ค๋
์ท ํ
์คํธ", () => {
const { asFragment } = render(<Counter />);
expect(asFragment()).toMatchSnapshot();
});
โ toMatchSnapshot()์ ์ฌ์ฉํ๋ฉด UI ๋ณ๊ฒฝ ๊ฐ์ง ๊ฐ๋ฅ
๐ ๊ณ ๊ธ ๊ธฐ๋ฅ
1. ์ปค์คํ ๋ ๋๋ฌ ์ฌ์ฉ๋ฒ
ํ ์คํธ๋ง๋ค ๊ณตํต์ ์ผ๋ก ํ์ํ Provider๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
import { render } from "@testing-library/react";
import { ThemeProvider } from "styled-components";
import theme from "../theme";
const customRender = (ui, options) =>
render(ui, { wrapper: ({ children }) => <ThemeProvider theme={theme}>{children}</ThemeProvider>, ...options });
export * from "@testing-library/react";
export { customRender as render };
์ด์ ๋ชจ๋ ํ ์คํธ์์ render() ๋์ customRender()๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
2. Mocking ๋ฐ Spying
API ์์ฒญ์ Mock ์ฒ๋ฆฌํ์ฌ ํ ์คํธํ ์ ์์ต๋๋ค.
๐ ์์ : API ์์ฒญ์ Mock ์ฒ๋ฆฌ
import { render, screen } from "@testing-library/react";
import axios from "axios";
import UserList from "../UserList";
jest.mock("axios");
test("API์์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํ๋ค", async () => {
axios.get.mockResolvedValue({ data: [{ id: 1, name: "Alice" }] });
render(<UserList />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
});
๐1๏ธโฃ ๋ชจ๋ฒ ์ฌ๋ก
โ 1๏ธโฃ ๋๋ฌด ๊ตฌ์ฒด์ ์ธ ๊ตฌํ์ ํ ์คํธํ์ง ๋ง ๊ฒ
โ ์๋ชป๋ ์:
expect(component.state.count).toBe(1); // ๋ด๋ถ ๊ตฌํ์ ํ
์คํธํ๋ฉด ์ ์ง๋ณด์ ์ด๋ ค์
โ ์ฌ๋ฐ๋ฅธ ์:
fireEvent.click(button);
expect(screen.getByText("์นด์ดํธ: 1")).toBeInTheDocument();
โ 2๏ธโฃ ID๋ณด๋ค ์ ๊ทผ์ฑ์ ๊ณ ๋ คํ ์ ๋ ํฐ ์ฌ์ฉ
โ ์๋ชป๋ ์:
screen.getByTestId("submit-button");
โ ์ฌ๋ฐ๋ฅธ ์:
screen.getByRole("button", { name: "์ ์ถ" });
โ 3๏ธโฃ ๋ค์ด๋ฐ ์ปจ๋ฒค์ ์ผ๊ด์ฑ ์ ์ง
describe("๋ก๊ทธ์ธ ๋ฒํผ", () => {
test("๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ก๊ทธ์ธ ์์ฒญ์ด ์ ์ก๋๋ค", () => {
// ํ
์คํธ ์ฝ๋...
});
});
๐ ์ ๋ฆฌ: React Testing Library ํต์ฌ ํฌ์ธํธ
โ “์ฌ์ฉ์ ์ ์ฅ์์ ํ ์คํธํ๋ผ!”
โ render() & screen์ ํ์ฉํด ์์ ์ฐพ๊ธฐ
โ userEvent๋ฅผ ์ฌ์ฉํด ์ค์ ์ฌ์ฉ์์ฒ๋ผ ํ ์คํธ
โ ๋น๋๊ธฐ ํ ์คํธ (findBy, waitFor) ํ์ ํ์ฉ
โ ํ ์คํธ ์คํจ ์ screen.debug()๋ก ๋๋ฒ๊น
โ ์ค๋ ์ท ํ ์คํธ๋ก UI ๋ณ๊ฒฝ ๊ฐ์ง
๐ท ์ ์ค์ ๊ฐ๋ฐ์๊ฐ ๋์ด๋ด ์๋น! ๐ท ๐