2. React Testing Library
React Testing Library
๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉ์ ์ ์ฅ์ ๊ฐ๊น๊ฒ ํ ์คํธ ํ ์ ์๋ ๋๊ตฌ
Implementation Driven Test(๊ตฌํ ์ฃผ๋ ํ ์คํธ) : ์ฃผ๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ด๋ป๊ฒ ์๋ํ๋์ง์ ๋ํด ์ด์ ์ ๋์ด ํ ์คํธ ์์ฑ
Behavior Driven Test(ํ์ ์ฃผ๋ ํ ์คํธ) : ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด์ฉํ๋ ๊ด์ ์์ ์ฌ์ฉ์์ ์ค์ ๊ฒฝํ ์์ฃผ๋ก ํ ์คํธ๋ฅผ ์์ฑ
React Testing Library๋ Behavior Driven Test ๋ฐฉ๋ฒ๋ก ์ ๋ฐ๋ฅด๋ ํ ์คํธ๋ฅผ ์์ฑํ๋๋ฐ ์ ํฉ
jsdom์ด๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํตํด ์ค์ ๋ธ๋ผ์ฐ์ DOM์ ๊ธฐ์ค์ผ๋ก ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ๋๋ฌธ์ด๋ค React ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํ๋์ง๋ณด๋ค, ์ฌ์ฉ์ ๋ธ๋ผ์ฐ์ ์์ ๋ ๋๋งํ๋ ์ค์ HTML ๋งํฌ์ ์ ๋ชจ์ต์ด ์ด๋ค์ง์ ๋ํด์ ํ ์คํธํ๊ธฐ ์ฉ์ด
Enzyme์ด๋ผ๋ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ฆฌ์กํธ VDOM์ ๊ธฐ์ค์ผ๋ก ํ ์คํธ๋ฅผ ์์ฑํ์ฌ Implementation Driven Test ๋ฐฉ๋ฒ๋ก ์ ๋ฐ๋ฆ
React Testing Library ์ฃผ์ API
render()ํจ์ : DOM์ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งfireEvent๊ฐ์ฒด : ํน์ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํด์ฟผ๋ฆฌํจ์ : DOM์์ ํน์ ์์ญ์ ์ ํ
render() ํจ์
render() ํจ์@testing-library/react๋ชจ๋๋ก๋ถํฐ ๋ฐ๋ก ์ํฌํธ ๊ฐ๋ฅ์ธ์๋ก ๋ ๋๋งํ React ์ปดํฌ๋ํธ๋ฅผ ๋๊น
React Testing Library์ฌ ์ ๊ณตํ๋ ๋ชจ๋ ์ฟผ๋ฆฌํจ์์ ๊ธฐํ ์ ํธ๋ฆฌํฐ ํจ์๋ฅผ ๋ด๊ณ ์๋ ๊ฐ์ฒด๋ฅผ ๋ฆฌํดํ๋ฏ๋ก destructuring ๋ฌธ๋ฒ์ผ๋ก
render()ํจ์๊ฐ ๋ฆฌํดํ ๊ฐ์ฒด๋ก๋ถํฐ ์ํ๋ ์ฟผ๋ฆฌํจ์๋ง ์ป์ด์ฌ ์ ์์
์ฟผ๋ฆฌํจ์
๋ ๋๋ง๋ DOM๋ ธ๋์ ์ ๊ทผํ์ฌ ์๋ฆฌ๋จผํธ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฉ์๋
์ฟผ๋ฆฌ ํ์/ํ๊ฒ ๊ฐ์/ํ๊ฒ ์ ํ์ผ๋ก ๊ตฌ์ฑ ex)get``All``ByRole
์ฟผ๋ฆฌํ์
get: ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฉฐ ํ๊ฒ์ ์ฐพ์ง๋ชปํ ์ ์๋ฌ๋ฅผ ๋์งfind: ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฉฐ ํ๊ฒ์ ์ฐพ์ง ๋ชปํ ์ ์๋ฌ๋ฅผ ๋์งquery: ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ฉฐ ํ๊ฒ์ ์ฐพ์ง ๋ชปํ ์null์ ๋ฐํ
ํ๊ฒ์ ๊ฐ์
๋ค์์ ์๋ฆฌ๋จผํธ๊ฐ ํ์๋๋ ์ํฉ์ด๋ผ๋ฉด ๋ค์
All์ ๋ถ์
ํ๊ฒ ์ ํ
ByRole,ByLabelText,ByPlaceholderText,ByText,ByDisplayValue,ByAltText,ByTitle,ByTestId
์ ์ ์ปดํฌ๋ํธ ํ
์คํ
Page Not Found ์ปดํฌ๋ํธ ์์
import React from "react";
function NotFound({ path }) {
return (
<>
<h2>Page Not Found</h2>
<p>ํด๋น ํ์ด์ง({path})๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.</p>
<img
alt="404"
src="https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif"
/>
</>
);
}h2๊ฐ ๋ ๋๋ง ๋๊ณ ์๋์ง ๊ฒ์ฆ
import React from "react";
import { render } from "@testing-library/react";
import NotFound from "./NotFound";
describe("<NotFound />", () => {
it("renders header", () => {
const { getByText } = render(<NotFound path="/abc" />);
const header = getByText("Page Not Found");
expect(header).toBeInTheDocument();
});
});<NotFound />์ปดํฌ๋ํธ๋ฅผ ์ํฌํธํด์render()์ ์ธ์๋ก ๋๊ธด ํ์ ๋ฆฌํด ๊ฐ์ฒด๋ก๋ถํฐgetByText()ํจ์๋ฅผ ์ป์ํ๋ฉด์์ ๊ฒ์ํ ํ ์คํธ์ธ
Page Not Found๋ฅผgetByText()์ ์ธ์๋ก ๋๊ฒจ<h2/>์๋ฆฌ๋จผํธ๋ฅผ ์ป์jest-dom์toBeInTheDocument()matcher ํจ์๋ฅผ ์ด์ฉํด์ ํด๋น<h2/>์๋ฆฌ๋จผํธ๊ฐ ํ๋ฉด์ ์กด์ฌํ๋ ๊ฒ์ฆ
p๊ฐ ๋ ๋๋ง ๋๊ณ ์๋์ง ๊ฒ์ฆ
it("renders paragraph", () => {
const { getByText } = render(<NotFound path="/abc" />);
const paragraph = getByText(/^ํด๋น ํ์ด์ง/);
expect(paragraph).toHaveTextContent("ํด๋น ํ์ด์ง(/abc)๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
});์ ๊ท์์ผ๋ก
<p/>์์ ํ ์คํธ๊ฐ ํฌํจ๋ ์์๋ฅผ ๊ฐ์ ธ์ด๊ฐ์ ธ์จ ์์์ ํ ์คํธ๊ฐ ์์๊ณผ ์ผ์นํ๋์ง ๊ฒ์ฆ
img๊ฐ ๋ ๋๋ง ๋๊ณ ์๋์ง ๊ฒ์ฆ
it("renders image", () => {
const { getByAltText } = render(<NotFound path="/abc" />);
const image = getByAltText("404");
expect(image).toHaveAttribute(
"src",
"https://media.giphy.com/media/14uQ3cOFteDaU/giphy.gif"
);
});์ด๋ฏธ์ง ํ๊ทธ๋ ํ ์คํธ๊ฐ ์๊ธฐ ๋๋ฌธ์
alt์์ฑ๊ฐ์ ์ด์ฉjest-dom์toHaveAttribute()matcher ํจ์๋ฅผ ์ด์ฉํด์<img/>์๋ฆฌ๋จผํธ์src์์ฑ๊ฐ์ด ์ ํํ์ง ๊ฒ์ฆ
๋์ ์ปดํฌ๋ํธ ํ
์คํ
๋ด๋ถ ์ํ์ ๋ฐ๋ผ UI์ ๋ณํ๊ฐ ์๊ธธ ์ ์๋ ์ปดํฌ๋ํธ ํ ์คํธ
import React, { useState } from "react";
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<>
<h2>Login</h2>
<form onSubmit={onSubmit}>
<label>
์ด๋ฉ์ผ
<input
type="email"
placeholder="user@test.com"
value={email}
onChange={({ target: { value } }) => setEmail(value)}
/>
</label>
<label>
๋น๋ฐ๋ฒํธ
<input
type="password"
value={password}
onChange={({ target: { value } }) => setPassword(value)}
/>
</label>
<button disabled={!email || !password}>๋ก๊ทธ์ธ</button>
</form>
</>
);
}์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํด์ผ ๋ก๊ทธ์ธ ๋ฒํผ์ด ํ์ฑํ ๋๋ ์์
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import LoginForm from "./LoginForm";
describe("<LoginForm />", () => {
it("enables button when both email and password are entered", () => {
const { getByText, getByLabelText } = render(
<LoginForm onSubmit={() => null} />
);
const button = getByText("๋ก๊ทธ์ธ");
const email = getByLabelText("์ด๋ฉ์ผ");
const password = getByLabelText("๋น๋ฐ๋ฒํธ");
expect(button).toBeDisabled();
fireEvent.change(email, { target: { value: "user@test.com" } });
fireEvent.change(password, { target: { value: "Test1234" } });
expect(button).toBeEnabled();
});
});๋ก๊ทธ์ธ ๋ฒํผ์ ๊ฒฝ์ฐ์๋
getByText()์ฟผ๋ฆฌ ํจ์๋ฅผ ํตํด ์ ํํ๊ณ , ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ ์ ๋ ฅ์นธ์getByLabelText()์ฟผ๋ฆฌ ํจ์๋ก ์ ํjest-dom์
toBeDisabled()์toBeEnabled()matcher ํจ์๋ฅผ ํตํด์ ๋ก๊ทธ์ธ ๋ฒํผ์ ํ์ฑํ ์ฌ๋ถ๋ฅผ ์ด๋ฒคํธ ๋ฐ์ ์ ํ๋ก ๊ฒ์ฆ๋ ๊ฐ์ ์ ๋ ฅ์นธ์ change ์ด๋ฒคํธ๋ฅผ ๋ฐ์์ํค๊ธฐ ์ํด์
fireEvent.change()ํจ์๋ฅผ ์ฌ์ฉ
๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํ์๋ prop์ผ๋ก ๋๊ธด onSubmit ํจ์๊ฐ ํธ์ถ๋๋์ง ๊ฒ์ฆ
it("submits form when buttion is clicked", () => {
const obSubmit = jest.fn();
const { getByText, getByLabelText } = render(
<LoginForm onSubmit={obSubmit} />
);
const button = getByText("๋ก๊ทธ์ธ");
const email = getByLabelText("์ด๋ฉ์ผ");
const password = getByLabelText("๋น๋ฐ๋ฒํธ");
fireEvent.change(email, { target: { value: "user@test.com" } });
fireEvent.change(password, { target: { value: "Test1234" } });
fireEvent.click(button);
expect(obSubmit).toHaveBeenCalledTimes(1);
});Mocking
์์ ์์ ์์ onSubmit์ ์ ๋ฌํ ํจ์๋ฅผ jest.fn()์ ์ด์ฉํด์ ๋ง๋ค์์
mocking: ๋จ์ํ ์คํธ๋ฅผ ์์ฑํ ๋ ํด๋น ์ฝ๋๊ฐ ์์กดํ๋ ๋ถ๋ถ์ ๊ฐ์ง(mock)๋ก ๋์ฒดํ๋ ๊ธฐ๋ฒ
jest.fn()
jest.fn() ํจ์๋ฅผ ์ด์ฉํด ๊ฐ์ง ํจ์๋ฅผ ์์ฑํ ์ ์์
์ผ๋ฐ ์๋ฐ์คํฌ๋ฆฝํธ ํจ์์ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ธ์๋ฅผ ๋๊ฒจ ํธ์ถํ ์ ์์
const mockFn = jest.fn();
mockFn();
mockFn(1);
mockFn("a");
mockFn([1, 2], { a: "b" });mockReturnValue(๋ฆฌํด๊ฐ) ํจ์๋ฅผ ์ด์ฉํด์ ์ด๋ค ๊ฐ์ ๋ฆฌํดํด์ผํ ์ง ์ ํด์ค ์ ์์
mockFn.mockReturnValue("I am a mock!");
console.log(mockFn()); // I am a mock!mockResolvedValue(Promise๊ฐ resolveํ๋ ๊ฐ) ํจ์๋ฅผ ์ด์ฉํด์ ๊ฐ์ง ๋น๋๊ธฐ ํจ์๋ฅผ ๋ง๋ค ์ ์์
mockFn.mockResolvedValue("I will be a mock!");
mockFn().then((result) => {
console.log(result); // I will be a mock!
});mockImplementation(๊ตฌํ ์ฝ๋) ํจ์๋ก ์ฌ๊ตฌํํ ์ ์์
mockFn.mockImplementation((name) => `I am ${name}!`);
console.log(mockFn("Dale")); // I am Dale!๊ฐ์งํจ์์ฉ Jest Matcher์ธ toBeCalled*** ํจ์๋ฅผ ์ด์ฉํด์ ๊ฐ์งํจ์๊ฐ ๋ช๋ฒ ํธ์ถ๋์๊ณ ์ธ์๋ก ๋ฌด์์ด ๋์ด์์๋์ง๋ฅผ ๊ฒ์ฆํ ์ ์์
mockFn("a");
mockFn(["b", "c"]);
expect(mockFn).toBeCalledTimes(2);
expect(mockFn).toBeCalledWith("a");
expect(mockFn).toBeCalledWith(["b", "c"]);jest.spyOn()
์ด๋ค ๊ฐ์ฒด์ ์ํ ํจ์์ ๊ตฌํ์ ๊ฐ์ง๋ก ๋์ฒดํ์ง ์๊ณ , ํด๋น ํจ์์ ํธ์ถ์ฌ๋ถ์ ์ด๋ป๊ฒ ํธ์ถ๋์๋์ง๋ง์ ์์๋ด์ผํ๋ ๊ฒฝ์ฐ
const calculator = {
add: (a, b) => a + b,
};
const spyFn = jest.spyOn(calculator, "add");
const result = calculator.add(2, 3);
expect(spyFn).toBeCalledTimes(1);
expect(spyFn).toBeCalledWith(2, 3);
expect(result).toBe(5);Given - When - Then
BDD
Behavior Driven Development
์๋๋ฆฌ์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์ค๋ฅด ์ผ์ด์ค๋ฅผ ์์ฑํ๋ฉฐ ํจ์๋จ์ ํ ์คํธ๋ฅผ ๊ถ์ฅํ์ง ์์
Given - When - Then ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ ๊ฒ์ ๊ถ์ฅ
Given - When - Then ํจํด
Given : ํ ์คํธ ์ํ๋ฅผ ์ค๋ช
When : ๊ตฌ์ฒดํํ๊ณ ์ ํ๋ ํ๋
Then : ์์๋๋ ๋ณํ
๊ธฐ๋ฅ : ์ฌ์ฉ์ ์ฃผ์ ํธ๋ ์ด๋
์๋๋ฆฌ์ค : ํธ๋ ์ด๋๊ฐ ๋ง๊ฐ๋๊ธฐ ์ ์ ์ฌ์ฉ์๊ฐ ํ๋งค๋ฅผ ์์ฒญ
"Given" ๋๋ MSFT ์ฃผ์์ 100๊ฐ์ง๊ณ ์๋ค.
๊ทธ๋ฆฌ๊ณ ๋๋ APPL ์ฃผ์์ 150๊ฐ์ง๊ณ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์๊ฐ์ ํธ๋ ์ด๋๊ฐ ์ข
๋ฃ๋๊ธฐ ์ ์ด๋ค.
"When" ๋๋ MSFT ์ฃผ์ 20์ ํ๋๋ก ์์ฒญํ๋ค.
"Then" ๋๋ MSFT ์ฃผ์ 80 ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค.
๊ทธ๋ฆฌ๊ณ ๋๋ APPL ์ฃผ์ 150์ ๊ฐ์ง๊ณ ์์ด์ผ ํ๋ค.
๊ทธ๋ฆฌ๊ณ MSFT ์ฃผ์ 20์ด ํ๋งค ์์ฒญ์ด ์คํ๋์์ด์ผ ํ๋ค.Test fixture
ํ ์คํธ ์คํ์ ์ํด ๋ฒ ์ด์ค๋ผ์ธ์ผ๋ก ์ฌ์ฉ๋๋ ๊ฐ์ฒด๋ค์ ๊ณ ์ ๋ ์ํ
๊ฒฐ๊ณผ๋ฅผ ๋ฐ๋ณต๊ฐ๋ฅํ ์ ์๊ณ ๊ณ ์ ๋ ํ๊ฒฝ์์ ํ ์คํธํ ์ ์๊ฒ ๋จ
์ค๋ณต ๋ฐ์๋๋ ํ์๋ฅผ ๊ณ ์ ์์ผ ํ๊ณณ์์ ๊ด๋ฆฌ
jest
beforeEach๋ฅผ ํตํด ํด๋น ํ์ผ ํน์ ํด๋์ค ๋ด๋ถ์ ํจ์๋ค์ด ์์ํ๊ธฐ ์ ์ ํญ์ ์ํํ ์ผ์ ์ง์ ํ ์ ์์(afterEach๋ ๊ฐ ํจ์ ์คํ ํ์)beforeAll(),afterAll()์ ๊ฐ๊ฐ ํจ์์ ์ ํ์ ๋งค๋ฒ ํธ์ถ๋๋ ๊ฒ์ด ์๋๋ผ ๋งจ ์ฒ์๊ณผ ๋งจ ๋์ ๋ฑ ํ ๋ฒ์ฉ๋ง ํธ์ถ๋จ
Last updated