[React] ๐Ÿ› React์—์„œ TDD ํ•˜๋Š” ๋ฐฉ๋ฒ•

TATAยท2023๋…„ 3์›” 29์ผ
0

React

๋ชฉ๋ก ๋ณด๊ธฐ
22/32

โ–ท TDD๋ž€?

Test-driven Development

ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ์—ฌ๋ถ€๋ฅผ
ํ™•์ธํ•˜๋ฉฐ ๊ฐœ๋ฐœํ•˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก ์„ ๋งํ•œ๋‹ค.

TDD๋ฅผ ํ†ตํ•ด ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๊ฐœ๋ฐœํ•œ๋‹ค๋Š” ๊ฒƒ์€
์ž‘์€ ๋‹จ์œ„์˜ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์ด๋ฅผ ํ†ต๊ณผํ•˜๋Š”
์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค.

๋•Œ๋ฌธ์— ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ–ˆ๋˜ ๋ฒ„๊ทธ๋ฅผ ์ค„์—ฌ์ค„ ์ˆ˜ ์žˆ๋‹ค.

TDD์˜ ๊ฐœ๋ฐœ ์ฃผ๊ธฐ 3๋‹จ๊ณ„
โ’ˆ Write Failing Test - ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑ
โ’‰ Make Test Pass - ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์„ฑ๊ณต์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ
โ’Š Refactor - ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ, ์ผ๋ฐ˜ํ™” ๋“ฑ์˜ ๋ฆฌํŒฉํ† ๋ง์„ ์ˆ˜ํ–‰

โ—๏ธTDD์˜ ์žฅ๋‹จ์ 

์žฅ์ ๋‹จ์ 
์ฝ”๋“œ์˜ ์‹ ๋ขฐ์„ฑ ํ–ฅ์ƒ๊ฐœ๋ฐœ ์ดˆ๊ธฐ ๋‹จ๊ณ„์— ์‹œ๊ฐ„๊ณผ ๋…ธ๋ ฅ์„ ๋งŽ์ด ์†Œ๋น„
๋ฒ„๊ทธ๋ฅผ ์ผ์ฐ ๋ฐœ๊ฒฌํ•˜์—ฌ ๋” ์ ์€ ์ˆ˜์ • ๋น„์šฉ ๋ฐœ์ƒํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜ ๋น„์šฉ์ด ์ถ”๊ฐ€๋จ
์ฝ”๋“œ ์ˆ˜์ • ์‹œ ๊ธฐ์กด ์ฝ”๋“œ์˜ ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ํšจ๊ณผ์ผ๋ถ€ ๋ณต์žกํ•œ ์ƒํ™ฉ์—์„œ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Œ
์„ค๊ณ„์™€ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ์ดํ•ด๋„ ํ–ฅ์ƒ๊ฐœ๋ฐœ์ž์˜ ์Šต๊ด€๊ณผ ๋Šฅ๋ ฅ์— ๋”ฐ๋ผ ์งˆ์ ์ธ ์ฐจ์ด ๋ฐœ์ƒ ๊ฐ€๋Šฅ
์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ๋กœ ์ฝ”๋“œ ์ž‘์„ฑ๊ณผ ๋™์‹œ์— ์ฝ”๋“œ ํ’ˆ์งˆ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Œํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์™€ ๋„๊ตฌ์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ํ•™์Šต์ด ํ•„์š”

โ–ท React์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

React์—์„œ ํ…Œ์ŠคํŠธ๋Š” Testing Library, Jest๋ฅผ ์ด์šฉํ•ด์„œ ํ•  ์ˆ˜ ์žˆ๋‹ค.
์„œ๋กœ ์—ญํ• ์ด ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ๋‘ ๊ฐœ๋ฅผ ๋‹ค ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

Testing Library
์‚ฌ์šฉ์ž ์ค‘์‹ฌ ํ…Œ์ŠคํŠธ(User-Centered Testing)๋ฅผ
๊ฐ•์กฐํ•˜๋Š” JavaScript์šฉ ํ…Œ์ŠคํŒ… ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

Jest
๋น ๋ฅด๊ณ  ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” JavaScript ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ,
ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ฐพ์•„ ์‹คํ–‰ํ•˜๊ณ , ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ•˜์—ฌ
์„ฑ๊ณต ๋˜๋Š” ์‹คํŒจ๋กœ ํŒ๋‹จํ•˜๋Š” ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณต

๐Ÿ›  start!

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 ๋ณด๋Ÿฌ๊ฐ€๊ธฐ

profile
๐Ÿพ

0๊ฐœ์˜ ๋Œ“๊ธ€