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