Test-driven Development
ํ
์คํธ๋ฅผ ๋จผ์ ์์ฑํ๊ณ ํ
์คํธ ํต๊ณผ ์ฌ๋ถ๋ฅผ
ํ์ธํ๋ฉฐ ๊ฐ๋ฐํ๋ ์ํํธ์จ์ด ๊ฐ๋ฐ ๋ฐฉ๋ฒ๋ก ์ ๋งํ๋ค.
TDD๋ฅผ ํตํด ์ํํธ์จ์ด๋ฅผ ๊ฐ๋ฐํ๋ค๋ ๊ฒ์
์์ ๋จ์์ ํ
์คํธ ์ผ์ด์ค๋ฅผ ์์ฑํ๊ณ , ์ด๋ฅผ ํต๊ณผํ๋
์ฝ๋๋ฅผ ์์ฑํ๋ ๊ณผ์ ์ ๋ฐ๋ณตํ๋ ๊ฒ์ ์๋ฏธํ๋ค.
๋๋ฌธ์ ์์ํ์ง ๋ชปํ๋ ๋ฒ๊ทธ๋ฅผ ์ค์ฌ์ค ์ ์๋ค.
TDD์ ๊ฐ๋ฐ ์ฃผ๊ธฐ 3๋จ๊ณ
โ Write Failing Test - ์คํจํ๋ ํ
์คํธ ์ฝ๋๋ฅผ ๋จผ์ ์์ฑ
โ Make Test Pass - ํ
์คํธ ์ฝ๋๋ฅผ ์ฑ๊ณต์ํค๊ธฐ ์ํ ์ค์ ์ฝ๋๋ฅผ ์์ฑ
โ Refactor - ์ค๋ณต ์ฝ๋ ์ ๊ฑฐ, ์ผ๋ฐํ ๋ฑ์ ๋ฆฌํฉํ ๋ง์ ์ํ
โ๏ธTDD์ ์ฅ๋จ์
์ฅ์ | ๋จ์ |
---|---|
์ฝ๋์ ์ ๋ขฐ์ฑ ํฅ์ | ๊ฐ๋ฐ ์ด๊ธฐ ๋จ๊ณ์ ์๊ฐ๊ณผ ๋ ธ๋ ฅ์ ๋ง์ด ์๋น |
๋ฒ๊ทธ๋ฅผ ์ผ์ฐ ๋ฐ๊ฒฌํ์ฌ ๋ ์ ์ ์์ ๋น์ฉ ๋ฐ์ | ํ ์คํธ ์ฝ๋ ์์ฑ๊ณผ ์ ์ง๋ณด์ ๋น์ฉ์ด ์ถ๊ฐ๋จ |
์ฝ๋ ์์ ์ ๊ธฐ์กด ์ฝ๋์ ๋ฌธ์ ๋ฅผ ๋ฐฉ์งํ๋ ํจ๊ณผ | ์ผ๋ถ ๋ณต์กํ ์ํฉ์์ ํ ์คํธ ์์ฑ์ด ์ด๋ ค์ธ ์ ์์ |
์ค๊ณ์ ๊ธฐ๋ฅ์ ๋ํ ๋ช ํํ ์ดํด๋ ํฅ์ | ๊ฐ๋ฐ์์ ์ต๊ด๊ณผ ๋ฅ๋ ฅ์ ๋ฐ๋ผ ์ง์ ์ธ ์ฐจ์ด ๋ฐ์ ๊ฐ๋ฅ |
์๋ํ๋ ํ ์คํธ๋ก ์ฝ๋ ์์ฑ๊ณผ ๋์์ ์ฝ๋ ํ์ง์ ์ ์งํ ์ ์์ | ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์ํ ํ๋ ์์ํฌ์ ๋๊ตฌ์ ๋ํ ์ถ๊ฐ ํ์ต์ด ํ์ |
React์์ ํ
์คํธ๋ Testing Library, Jest๋ฅผ ์ด์ฉํด์ ํ ์ ์๋ค.
์๋ก ์ญํ ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ ๊ฐ๋ฅผ ๋ค ์ฌ์ฉํ์ฌ ํ
์คํธ๋ฅผ ํ๋ ๊ฒ์ด ์ข๋ค.
Testing Library
์ฌ์ฉ์ ์ค์ฌ ํ
์คํธ(User-Centered Testing)๋ฅผ
๊ฐ์กฐํ๋ JavaScript์ฉ ํ
์คํ
์ ํธ๋ฆฌํฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
Jest
๋น ๋ฅด๊ณ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ JavaScript ํ
์คํ
ํ๋ ์์ํฌ,
ํ
์คํธ ํ์ผ์ ์๋์ผ๋ก ์ฐพ์ ์คํํ๊ณ , ๊ฒฐ๊ณผ๋ฅผ ๋น๊ตํ์ฌ
์ฑ๊ณต ๋๋ ์คํจ๋ก ํ๋จํ๋ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณต
CRA๋ก ๋ฆฌ์กํธ ํด๋ ์์ฑ
/* package.json */
// CRA๋ก ๋ฆฌ์กํธ ํด๋๋ฅผ ์์ฑํ๋ฉด, testing์ ๋ฐ๋ก ์ค์นํ์ง ์์๋ ๋จ
"dependencies": {
"@testing-library/jest-dom": "^5.16.5", // jest-dom์์ ์ ๊ณตํ๋ custom matcher๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ค
"@testing-library/react": "^13.4.0", // ์ปดํฌ๋ํธ์ ์์๋ฅผ ์ฐพ๊ธฐ ์ํ query๊ฐ ํฌํจ๋์ด ์์
"@testing-library/user-event": "^13.5.0", // click ๋ฑ ์ฌ์ฉ์ ์ด๋ฒคํธ์ ์ด์ฉ๋จ
},
test ํจ์, expect ํจ์๋ Jest์ ํจ์์ด๋ค.
toBeInTheDocument๋ jest-dom ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ํฌํจ๋ Custom matchers์ด๋ค.
/* App.test.js */
import { render, screen } from '@testing-library/react';
import App from './App';
// test ํจ์์ ์ฒซ ๋ฒ์งธ ์ธ์๋ ์ค๋ช
์ ์์ฑ
// ๋๋ฒ์งธ ์ธ์๋ ํ๊ณ ์ ํ๋ ํ
์คํธ๋ฅผ ํจ์์ ํํ๋ก ๋ฃ๋๋ค.
test('renders learn react link', () => {
render(<App />); // react-testing-library์์๋ ํ
์คํธ๋ฅผ ์งํํ ์ปดํฌ๋ํธ๋ฅผ render()ํจ์์ ์ธ์๋ก ์ ๋ฌํ๋ค.
const linkElement = screen.getByText(/learn react/i); // screen์ ๋ค์ํ ๋ฉ์๋ ์ค getByText() ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ render()์์ ๊ฐ์ ธ์จ App ์ปดํฌ๋ํธ ์ค "learn react"๋ผ๋ ๋ฌธ์์ด์ด ์๋์ง ํ์ธํ์ฌ linkElement์ ํ ๋นํ๊ณ ์๋ค.
expect(linkElement).toBeInTheDocument(); // expect ํจ์์ ์ธ์๋ก ์ง์ ํ ์์๊ฐ document.body์ ์กด์ฌํ๋์ง toBeInTheDocument ํจ์(matchers ํจ์)๋ฅผ ์ฌ์ฉํ์ฌ ์ฒดํฌํ๊ณ ์๋ค.
});
srcํด๋์ Example.test.js ํ์ผ์ ์์ฑ
(ํ
์คํธ๋ฅผ ์คํํ๊ธฐ ์ํด์๋ ํ์ผ๋ช
์ <ํ์ผ๋ช
>.test.js๋ก ์์ฑํ๋ฉด ๋จ)
describe
ํจ์ ๋ธ๋ก์ Test Suites๋ผ๊ณ ๋ถ๋ฆฌ๋ฉฐ
test
it
ํจ์ ๋ธ๋ก์ Test(Test Case)๋ผ๊ณ ํ๋ค.
/* Example.test.js */
/* <ํ์ผ๋ช
>.spec.js์ผ๋ก ์์ฑํด๋ ๋จ */
test("2 ๋ํ๊ธฐ 2๋ 4", () => {
expect(2 + 2).toBe(4);
});
// test ํจ์ ๋์ it ํจ์๋ฅผ ์ฌ์ฉํด๋ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์จ๋ค.
-------
// describeํจ์๋ฅผ ์ฌ์ฉํ๋ฉด itํจ์๋ testํจ์๋ฅผ ํ๋์ ํ์ผ์ ์ฌ๋ฌ ๊ฐ ํฌํจํ ์ ์๋ค.
describe("๊ฐ๋จํ ํ
์คํธ๋ค", () => {
test("2 ๋ํ๊ธฐ 2๋ 4", () => {
expect(2 + 2).toBe(4);
});
it("2๋นผ๊ธฐ 1์ 1", () => {
expect(2 - 1).toBe(1);
});
});
์ปดํฌ๋ํธ ๋ง๋ค๊ธฐ
srcํด๋์ componentsํด๋๋ฅผ ์์ฑํ๊ณ
Light.jsx ํ์ผ์ ์์ฑํ ํ, App.test.js ํ์ผ์ ์ญ์
/* Light.jsx */
import { useState } from "react";
// ์ ์์ ์ํ๋ฅผ OFF์์ ON์ผ๋ก ์ ํํ๋ ์ปดํฌ๋ํธ
function Light({ name }) {
const [light, setLight] = useState(false);
return (
<div>
<h1>
{name} {light ? "ON" : "OFF"}{" "}
</h1>
<button onClick={() => setLight(true)} disabled={light ? true : false}>
ON
</button>
<button onClick={() => setLight(false)} disabled={!light ? true : false}>
OFF
</button>
</div>
);
}
-------
/* App.js */
import "./App.css";
import Light from "./components/Light";
function App() {
return <Light name="์ ์" />;
}
์ปดํฌ๋ํธ ํ ์คํธํ๊ธฐ
compoents ํด๋ ์๋์ Light.test.js ํ์ผ ์์ฑ
import { render, screen } from "@testing-library/react";
import Light from "./Light";
// PASS
it("renders Light Component", () => {
render(<Light name="์ ์" />);
const nameElement = screen.getByText(/์ ์ off/i);
expect(nameElement).toBeInTheDocument();
});
// PASS
it("off button disabled", () => {
render(<Light name="์ ์" />);
const offButtonElement = screen.getByRole("button", { name: "OFF" });
expect(offButtonElement).toBeDisabled(); // getByRole๊ณผ Disabled์ฌ์ฉ
});
// PASS
it('on button enable', () => {
render(<Light name="์ ์" />);
const onButtonElement = screen.getByRole('button', { name: 'ON' });
expect(onButtonElement).not.toBeDisabled(); // not์ ์ฌ์ฉ
});
fireEvent๋ฅผ ์ฌ์ฉํ์ฌ ๋ฒํผ ํด๋ฆญ
์ด๋ฒคํธ์ ์ ๋ฌด๋ ํ
์คํธ๋ก ๊ตฌํํ ์ ์๋ค.
// fireEvent ๊ฐ์ ธ์ค๊ธฐ
import { fireEvent, render, screen } from '@testing-library/react';
import Light from './Light';
it('change from off to on', () => {
render(<Light name="์ ์" />);
const onButtonElement = screen.getByRole('button', { name: 'ON' });
fireEvent.click(onButtonElement); // fireEvent์ click๋ฉ์๋์ ์ ๋ฌ์ธ์๋ก ํ
์คํธํ๊ณ ์ ํ๋ ์์๋ฅผ ์ ๋ฌํ๋ค.
expect(onButtonElement).toBeDisabled();
})
๐ ์ต๊ทผ ์ด์ฉ๋ฅ โ ํ ์คํธ ํ๋ ์์ํฌ์ธ Vitest ๋ณด๋ฌ๊ฐ๊ธฐ