์ RTL์ธ๊ฐ?
React ์ปดํฌ๋ํธ ํ
์คํธ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค.
๊ณผ๊ฑฐ์ Enzyme์ ์ฌ์ฉํ์ ์ ์๋ค. ๋ค๋ฅธ ์ ์ React ์ปดํฌ๋ํธ์ ์ธ์คํด์ค๊ฐ ์๋ ์ค์ DOM ๋
ธ๋๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ด๋ค.
์น ๋ธ๋ผ์ฐ์ ํ๊ฒฝ๊ณผ ์ ์ฌํ ํ๊ฒฝ์์ ํ
์คํธ ์ผ์ด์ค๊ฐ ์คํ๋๋ค๋ ๊ฒ์ ์๋ฏธํ๋ค. ๋น์ทํ ์๋ก ์ ๋ขฐํ ์ ์๋ค.
๋ ๋ค๋ฅธ ์ด์ ๋ ์ฌ์ฉ์๊ฐ ์ฑ๊ณผ ์ธํฐ๋์
ํ๋ ๋ฐฉ์๊ณผ ์ ์ฌํด์ผ ํ๋ค๋ ๊ธฐ๋ณธ์ ์ธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฒ ํ ๋๋ฌธ์ด๋ค.
์ฌ์ฉ์๋ state, props์ ์ธํฐ๋์
ํ๋ ๊ฒ์ ๋ชจ๋ฅธ๋ค.
ํจ์ ์ปดํฌ๋ํธ์์ ํ
์ ์ฌ์ฉํ๋์ง, ํด๋์ค ์ปดํฌ๋ํธ์ ํจ๊ป ๊ณ ์ฐจ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋์ง ์ ๊ฒฝ ์ฐ์ง ์๋๋ค.
๊ทธ์ ์ธํฐํ์ด์ค๋ฅผ ๋ณด๊ณ ์ธํฐ๋์
ํ๋ค.
์ฌ๋ฐ๋ฅธ props, state์ธ์ง ํ
์คํธํ๋ ๊ฒ ๋์ ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์ํํ๋ ์์
์ ํ
์คํธํ๋๋ก ์ค๊ณ๋์๋ค. ๋ฐ๋ผ์ ์ ๊ทผ ๊ฐ๋ฅํ UI๋ฅผ ๊ตฌ์ถํ๊ณ HTML์ ๊ตฌ์ฑํ ๋ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์ค์ํ ์ ์๋ค.
์ฒ ํ ์ ์ฉํ๊ธฐ
ํ ์คํธ์ผ์ด์ค 1
์ฑ์ด ๋ก๋ฉ๋๋ฉด ์ฌ์ฉ์๊ฐ ๊ฐ์ฅ ๋จผ์ ๋ณด๊ฒ ๋๋ ๊ฒ์ ๋ฌด์์ธ๊ฐ?
์ด๋ฆ๊ณผ ์ฑ์ ์
๋ ฅํ๋ ์ ๋ชฉ๊ณผ 21์ธ ์ด์์ธ์ง ๋ฌป๋ ์ฒดํฌ ๋ฐ์ค์ ์ทจ์/์ ์ถ ๋ฒํผ์ด๋ค.
์ฌ์ฉ์๊ฐ ์ฒ์์ ๋ณด๊ฒ ๋ ๊ฒ์ ํ
์คํธํ๋ ๊ฒ์ ์ข์ํ๋ค.
ํ ์คํธ์ผ์ด์ค 2
๋ค์ ์ธํฐ๋์
์ Form ์์ฑ์ ํ๋ ๊ฒ์ด๋ค. ๋ฐ๋ผ์ ์ฌ์ฉ์๊ฐ Form์ ์์ฑํ๊ธฐ ์์ํ๋ฉด โ21์ธ ์ด์์
๋๊น?โ๋ผ๋ ๋ฉ์์ง์ ์ฒดํฌ๋ฐ์ค๊ฐ ํ์๋๋ค.
์ฌ์ฉ์๊ฐ ์ฒดํฌ๋ฐ์ค๋ฅผ ํด๋ฆญํ๋ฉด ์ข์ํ๋ ์๋ฃ๋ฅผ ์
๋ ฅํ ์ ์๋๋ก ์กฐ๊ฑด๋ถ๋ก ๋ ๋ค๋ฅธ ์
๋ ฅ์ด ๋ํ๋๋ค.
์ด๊ฑด ํ
์คํธํด์ผ ํ๋ ๋ณ๋์ ์ฝ๋ ๋ถ๊ธฐ๋ฅผ ๋ํ๋ธ๋ค.
์ด ํ
์คํธ๊ฐ ์ด๋ป๊ฒ useState์ ์ฌ์ฉ์ ์ง์ ์ ์ผ๋ก ํ
์คํธํ์ง ์๋์ง ๋์ฌ๊ฒจ๋ณด๋ผ.
์ฐ๋ฆฌ๋ ๋ด๋ถ state๊ฐ true๋ false๋ก ๋ฐ๋๋ ๊ฒ์ด ์๋ ์ฌ์ฉ์๊ฐ ์ฌ๋ฐ๋ฅธ ์ ๋ณด๋ฅผ ๋ณด๋์ง ํ
์คํธํ๋ ค๋ ๊ฒ์ด๋ค.
useReducer๋ ๋ค๋ฅธ ์ํ ๊ด๋ฆฌ ์๋ฃจ์
์ ์ฌ์ฉํ๋๋ก ๋ฆฌํฉํฐ๋งํด๋ ํ
์คํธ๋ ๋ณ๊ฒฝํ ํ์๊ฐ ์๋ค.
ํ ์คํธ์ผ์ด์ค 3, 4
์ฌ์ฉ์๊ฐ ๋ง์ง๋ง์ผ๋ก ํ ์ ์๋ ๊ฑด ์ทจ์/์ ์ถ์ด๋ค. ๋ถ๋ชจ ์ปดํฌ๋ํธ์์ ์ฝ๋ฐฑํจ์๋ฅผ ๋๊ฒจ์ฃผ๋ ๋ฐฉ์์ด๋ค. ์ด์ ํ
์คํธ ์ผ์ด์ค์ ์ฝ๊ฐ ๋ค๋ฅด๋ค.
์ฌ์ฉ์๊ฐ ๋ณด๋ ๊ฒ์ ํ
์คํธํ๋ ๊ฒ์ด ์๋๋ผ ํน์ ํจ์๋ฅผ ํธ์ถํด ์ฌ์ฉ์์ ์์
์ ๋ด๋ถ์ ์ผ๋ก ์ฌ๋ฐ๋ฅด๊ฒ ๋ฐ์ํ๋์ง ํ
์คํธํ๋ค.
์ ์ธ์ ํ๋ก๊ทธ๋๋ฐ์ ์ฌ์ฉํ์ฌ ํ ์คํธ ์์ฑํ๊ธฐ
it("๋ญ๊ฐ ์ํํ๋ค.", async () => {
const onSubmit = jest.fn();
const onCancel = jest.fn();
const result = render(<ComplexForm onSubmit={onSubmit} onCancel={onCancel} />);
expect(result.getByLabelText('First Name')).toBeInTheDocument();
expect(result.getByLabelText('Last Name')).toBeInTheDocument();
await act(async () => {
userEvent.click(result.getByLabelText('Over 21?'));
});
expect(result.getByLabelText('Favorite Drink?')).toBeInTheDocument();
});
TypeScript
๋ณต์ฌ
๊ฐ๋จํ ์ปดํฌ๋ํธ, 1~2๊ฐ ํ
์คํธ๋ฉด ๊ด์ฐฎ๋ค.
์ด๋ฐ ํ
์คํธ๋ ํ์ผ์ด ํฌ๊ณ ๋น ๋ฅด๊ฒ ์ดํดํ๊ธฐ ์ด๋ ต๋ค. ์งํ์ํฉ์ ์ดํดํ๊ธฐ ์ํด ์ฝ๋ ๋ผ์ธ์ ์ฃผ์๊น๊ฒ ์ฝ์ด์ผ ํ๋ค.
ํ๋ ์ผ์ ๋ณด์ฌ์ฃผ๋ ํ
์คํธ ๋์ ์ฌ์ฉ์์ ์๋๋ฅผ ์ค๋ช
ํ๋ ํ
์คํธ๋ฅผ ์์ฑํ์.
it("๋ญ๊ฐ ์ํํ๋ค.", async () => {
const { FirstNameInput, LastNameInput, clickIsOver21, FavoriteDrinkInput } = renderComplexForm();
// ์ด๋ฆ ์
๋ ฅ, ์ฑ ์
๋ ฅ, 21์ด์์ธ์ง ์ฒดํฌ๋ฐ์ค, ์ฆ๊ฒจ ์ฐพ๋ ์๋ฃ ์
๋ ฅ
expect(FirstNameInput()).toBeInTheDocument();
expect(LastNameInput()).toBeInTheDocument();
// ์
๋ ฅ 2๊ฐ ์ ๋ค์ด์ค๋?
await clickIsOver21();
// ํด๋ฆญ์ ๋น๋๊ธฐ๋ก ๊ธฐ๋ค๋ฆผ
expect(FavoriteDrinkInput()).toBeInTheDocument();
// ์
๋ ฅ ์ ๋ค์ด์ค๋?
});
TypeScript
๋ณต์ฌ
ํจ์๋ง ์ฝ์ด๋ ํ์ฌ ์ํฉ์ ์ฆ์ ํ์
ํ ์ ์๋ค.
์ด๋ฆ๊ณผ ์ฑ ์
๋ ฅ์ด doc์ ์๋์ง ํ์ธํ๋ค. ๊ทธ ๋ค์ โ21์ธ ์ด์์
๋๊น?โ ์ฒดํฌ๋ฐ์ค๋ฅผ ํด๋ฆญํ ๋ค์ ์ฆ๊ฒจ ์ฐพ๋ ์๋ฃ ์
๋ ฅ์ด doc์ ์๋์ง ํ์ธํ๋ค.
์ฝ๊ธฐ ์ฝ๊ณ renderComplexForm ํจ์์์ ๋ด๋ณด๋ธ ํ
์คํธ ํฌํผ๋ฅผ ๋ค๋ฅธ ํ
์คํธ ์ผ์ด์ค์์ ์ฌ์ฌ์ฉํ ์ ์๋ค.
์ฝ๋ ์ค๋ณต์ ์ค์ด๊ณ ๊ฐ๋
์ฑ์ ํฌ๊ฒ ๋์ฌ์ผ ํ๋ค. ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ณ ํ
์คํธ๋ฅผ ์
๋ฐ์ดํธํ ๋ ํจ์ฌ ์ฝ๋ค.
๋๊ท๋ชจ ํ๋ก์ ํธ์์ ๋งค์ฐ ์ํํ๊ฒ ํ์ฅํ๊ณ ๋ณต์กํ ์ปดํฌ๋ํธ๋ฅผ ์ฝ๊ฒ ํ
์คํธํ ์ ์๋ค.
ComplexForm ์ปดํฌ๋ํธ์ ๋ํ ํ ์คํธ ์์ฑ
import React from "react";
import userEvent from "@testing-library/user-event";
import { act, render } from "@testing-library/react";
import ComplexForm, { ComplexFormProps } from "./ComplexForm";
/**
* ์ด๊ฑด ๋ชจ๋ ํ
์คํธ์์ ํธ์ถ๋๋ ํ
์คํธ ์ค์ ์ด๋ค.
*
* ํ
์คํธ ์ค์ ํจ์๋ฅผ ๋ง๋ค๋ฉด ํ
์คํธ ์ผ์ด์ค์ ๋ํด ์์ฑํด์ผ ํ๋ ๋ฐ๋ณต ์ฝ๋์ ์์ด
* ์ค์ด๋ค๊ณ ํ
์คํธ์ค์ธ ์ปดํฌ๋ํธ์ ์ํธ์์ฉํ๊ธฐ ์ํ ์ ์ธ์ ํ
์คํธ ํฌํผ๋ฅผ ์ค์ ํ ์ ์๋ค.
*/
function renderComplexForm(props?: Partial<ComplexFormProps>) {
/* ์ ์ถ๊ณผ ์ทจ์ ๋ฒํผ์ ์ํ mock ์ฝ๋ฐฑ ํจ์๋ฅผ ์ค์ ํ๋ค. */
const onSubmit = jest.fn();
const onCancel = jest.fn();
/* React Testing Library๋ฅผ ์ฌ์ฉํด ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ค. */
const result = render(<ComplexForm onSubmit={onSubmit} onCancel={onCancel} {...props} />);
/* ๋ค์ 7๊ฐ์ ํจ์๋ ์ปดํฌ๋ํธ์์ ๊ณตํต DOM ์์๋ฅผ ๊ฐ์ ธ์ค๊ธฐ ์ํ ํฌํผ ํจ์์ด๋ค. */
const Heading = () => result.getByText("Welcome, Zerry");
const FirstNameInput = () => result.getByLabelText("First Name");
const LastNameInput = () => result.getByLabelText("Last Name");
const IsOver21Input = () =>
result.getByLabelText("Are you at least 21 years old?");
const FavoriteDrinkInput = () => result.queryByLabelText("What's your favorite drink?");
const CancelButton = () => result.getByText("Cancel");
const SubmitButton = () => result.getByText("Apply");
/* ๋ค์ 6๊ฐ์ ํจ์๋ DOM ์์์ ์ํธ์์ฉํ๊ธฐ ์ํ ํฌํผ ํจ์์ด๋ค. */
function changeFirstName(name: string) {
userEvent.type(FirstNameInput(), name);
}
function changeLastName(name: string) {
userEvent.type(LastNameInput(), name);
}
function changeFavoriteDrinkInput(name: string) {
userEvent.type(FavoriteDrinkInput() as HTMLElement, name);
}
async function clickIsOver21() {
await act(async () => {
userEvent.click(IsOver21Input());
});
}
function clickSubmit() {
userEvent.click(SubmitButton());
}
function clickCancel() {
userEvent.click(CancelButton());
}
/*
๋ง์ง๋ง์ผ๋ก ์ด ์ ํธ๋ฆฌํฐ ๋ ๋ ํจ์์์ ๋ชจ๋ ํจ์์ ์์๋ฅผ ๋ด๋ณด๋ธ๋ค. ์ด๋ฅผ ํตํด ๋ชจ๋
ํ
์คํธ ์ผ์ด์ค์์ ํ์ํ ๊ฒ์ ์ป์ ์ ์๋ค.
*/
return {
result,
onSubmit,
changeFirstName,
changeLastName,
clickIsOver21,
clickSubmit,
clickCancel,
FirstNameInput,
LastNameInput,
IsOver21Input,
SubmitButton,
CancelButton,
Heading,
FavoriteDrinkInput,
changeFavoriteDrinkInput,
onCancel,
};
}
describe("<ComplexForm />", () => {
it("๊ธฐ๋ณธ ํ๋๋ฅผ ๋ ๋๋งํด์ผ ํ๋ค.", async () => {
const {
FirstNameInput,
LastNameInput,
IsOver21Input,
SubmitButton,
Heading,
FavoriteDrinkInput,
CancelButton
} = renderComplexForm();
// ํค๋
expect(Heading()).toBeInTheDocument();
// ์
๋ ฅ
expect(FirstNameInput()).toBeInTheDocument();
expect(LastNameInput()).toBeInTheDocument();
expect(IsOver21Input()).toBeInTheDocument();
expect(FavoriteDrinkInput()).not.toBeInTheDocument();
// ๋ฒํผ๋ค
expect(CancelButton()).toBeInTheDocument();
expect(SubmitButton()).toBeInTheDocument();
});
it("21์ธ ์ด์ ์ฒดํฌ ์ฌ๋ถ์ ๋ฐ๋ผ ์ข์ํ๋ ์๋ฃ ์
๋ ฅ์ ํ ๊ธํด์ผํ๋ค.", async () => {
const { clickIsOver21, FavoriteDrinkInput } = renderComplexForm();
expect(FavoriteDrinkInput()).not.toBeInTheDocument();
await clickIsOver21();
expect(FavoriteDrinkInput()).toBeInTheDocument();
});
it("์ทจ์ ๋ฒํผ์ด ํด๋ฆญ๋๋ฉด onCancel ํจ์๊ฐ ํธ์ถ๋์ผ ํ๋ค.", async () => {
const { clickCancel, onCancel } = renderComplexForm();
clickCancel();
expect(onCancel).toHaveBeenCalled();
});
it("form ๊ฐ์ผ๋ก onSubmit์ ํธ์ถํด์ผ ํ๋ค.", async () => {
const {
changeFirstName,
changeLastName,
clickIsOver21,
changeFavoriteDrinkInput,
clickSubmit,
onSubmit
} = renderComplexForm();
changeFirstName('Zerry');
changeLastName('Hogan');
await clickIsOver21();
changeFavoriteDrinkInput('Bourbon');
clickSubmit();
expect(onSubmit).toHaveBeenCalledWith({
first_name: 'Zerry',
last_name: 'Hogan',
is_over_21: true,
favorite_drink: 'Bourbon',
});
});
});
TypeScript
๋ณต์ฌ
๋๋ ์ ์ค๋ช
ํ๊ฒ ๋ค.
1.
๋จผ์ ์ปดํฌ๋ํธ์ ๋ํ ๋ ๋๋ง ํจ์๋ฅผ ๋ง๋ ๋ค. render ํจ์๋ React Testing Library๋ฅผ ์ฌ์ฉํด ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๊ณ ํ
์คํธ ์ผ์ด์ค๋ฅผ ์ํ ํฌํผ ํจ์๋ฅผ ๋ด๋ณด๋ด๋ ์ญํ ์ ํ๋ค. ๋ ๋๋ง ๊ธฐ๋ฅ์ ์ํ ๋ณ๋์ ํ์ผ์ ๋ง๋ค์ด ํ
์คํธ๋ก ๊ฐ์ ธ์ฌ ์๋ ์๋ค.
2.
๊ฐ ํ
์คํธ ์ผ์ด์ค๋ย renderComplexForm์ ํธ์ถํด ํน์ ํ
์คํธ ์ผ์ด์ค์ ํ์ํ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ๊ฐ์ ธ์จ๋ค.
3.
์
๋ ฅ ๊ฐ์ ๋ณ๊ฒฝํ๊ธฐ ์ํดย changeFirstnameย ํ
์คํธ ํฌํผ ํจ์๋ฅผ ๋ง๋ค์๋ค. ์ฌ์ฉ์๊ฐ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ์๋ฎฌ๋ ์ด์
ํ๊ณ ํ
์คํธ์์ ์ด๋ค ์ผ์ด ์ผ์ด๋๋์ง ๋ช
๋ฐฑํ ๋ณด์ฌ์ค๋ค.
4.
renderComplexFormย ํจ์๋ย propsย ์ธ์๋ฅผ ๋ฐ๋๋ค. ์ปดํฌ๋ํธ๊ฐ ์ปดํฌ๋ํธ์ UI ๋๋ ์ฌ์ฉ์๊ฐ ๋ณด๋ ๊ฒ์ ๋ณ๊ฒฝํ๋ props๋ฅผ ๋ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. ๊ฐ ํ
์คํธ ์ผ์ด์ค๊ฐย props๋ฅผ ๋๊ธฐ๋๋ก ํ์ฉํด ์๋ก ๋ค๋ฅธ ์ํธ์์ฉ์ ํ
์คํธํ ์๋ ์๋ค.
5.
onSubmit๊ณผย onCancelย props๋ฅผ ์ํด jest mock ํจ์๋ฅผ ์ฌ์ฉํ๊ณ ์๋ค. jest mock ํจ์๋ ํจ์๊ฐ ํธ์ถ๋์๋์ง, ๋ช ๋ฒ์ด๋ ํธ์ถ๋์๋์ง, ๊ทธ๋ฆฌ๊ณ ์ด๋ค ์ธ์๋ก ํธ์ถ๋์๋์ง ํ
์คํธํ๋ ๋ฐ ์ ์ฉํ๋ค. ๋ง์ง๋ง ๋ ํ
์คํธ ์ผ์ด์ค์์ ๋ฒํผ ํด๋ฆญ ์ ์ ์ ํ ์ฝ๋ฐฑ ํจ์๋ฅผ ํธ์ถํ๋์ง ํ
์คํธํ๊ธฐ ์ํด jest mock ํจ์๋ฅผ ์ฌ์ฉํ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ๋์ฑ ์ฝ๊ธฐ ์ฝ๊ณ ํ์ฅ ๊ฐ๋ฅํ๋ฉฐ ์ค๋ ์ง์๋ ์ ์๋ ํ
์คํธ๋ฅผ ๋ง๋ค์๋ค๊ณ ๋ฏฟ๋๋ค. ์ด ํ
์คํธ๋ก ๋์์์ ์
๋ ฅ์ ์ถ๊ฐํ๊ณ ํ
์คํธ ์ฝ๋๋ฅผ ๊ตฌ๋ฌธ ๋ถ์ํ์ฌ ์ ํ
์คํธ ์ ์ฉ ๋ฒ์๋ฅผ ์ถ๊ฐํ ์์น๋ฅผ ์ ํ์ ์์ด ๋ช ๋ถ ์์ ํ
์คํธ๋ฅผ ์
๋ฐ์ดํธํ ์ ์๋ค.
GitHub repo:ย https://github.com/jerrywithaz/how-to-test-react-app