Jieunny์˜ ๋ธ”๋กœ๊ทธ

S4) Unit 7. [Testing] TDD ๋ณธ๋ฌธ

CodeStates/learning contents

S4) Unit 7. [Testing] TDD

Jieunny 2023. 3. 29. 16:00

๐Ÿ“ฃ TDD

๐Ÿญ. TDD(Test-driven Development)๋ž€?

โœ”๏ธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ํ…Œ์ŠคํŠธ๋ฅผ ์“ฐ๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก 

โžฐ ๊ฐœ๋ฐœ์ž ์ž์‹ ์ด ๋ฐ”๋žŒ์งํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š” ์ฝ”๋“œ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฏธ๋ฆฌ ์ •์˜ํ•˜๊ณ , ์ด๊ฒƒ์„ ๋ฐ”ํƒ•์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•

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

TDD์˜ ๊ฐœ๋ฐœ์ฃผ๊ธฐ

 

๐Ÿฎ. TDD์˜ ๊ฐœ๋ฐœ์ฃผ๊ธฐ

1๏ธโƒฃ Write Failing Test: ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•œ๋‹ค.

2๏ธโƒฃ Make Test Pass: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์„ฑ๊ณต์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

3๏ธโƒฃ Refactor: ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ, ์ผ๋ฐ˜ํ™” ๋“ฑ์˜ ๋ฆฌํŒฉํ† ๋ง์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

โžฐ 1์˜ ๊ณผ์ •์„ ๋งˆ์น˜๊ธฐ ์ „์— 2์˜ ์ž‘์—…์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.

โžฐ 2๋ฅผ ์ง„ํ–‰ํ•  ๋•Œ์—๋Š”, 1์˜ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•  ์ •๋„์˜ ์ตœ์†Œ ์ฝ”๋“œ๋งŒ ์ž‘์„ฑํ•ด์•ผ ํ•œ๋‹ค.

 

๐Ÿฏ. TDD๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

โœ”๏ธ ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ–ˆ๋˜ ๋ฒ„๊ทธ๋ฅผ ์ค„์—ฌ ์†Œ์š” ์‹œ๊ฐ„์„ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ“ฃ React ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

โœ”๏ธ React์—์„œ ํ…Œ์ŠคํŠธ๋Š” Testing Library, Jest๋ฅผ ์ด์šฉํ•ด์„œ ํ•  ์ˆ˜ ์žˆ๋‹ค.

โžฐ create-react-app์„ ์ด์šฉํ•˜์—ฌ React ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์ž๋™์œผ๋กœ Testing Libarary๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

    ใ„ด Testing Library๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์€ ์ปดํฌ๋„ŒํŠธ๋‚˜ ํด๋ฆญ ์ด๋ฒคํŠธ ๋“ฑ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

โžฐ Jest : JavaScript์˜ Testing Framework / Test Runner๋กœ์จ, ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ์ฐพ์•„ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ณ , ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ ๊ธฐ๋Œ€๋งŒํผ ์˜ฌ๋ฐ”๋ฅธ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•˜์—ฌ ์ฒดํฌํ•˜์—ฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต์ธ์ง€ ์‹คํŒจ์ธ์ง€๋ฅผ ํŒ๋‹จํ•ด์ค€๋‹ค.

 

๐Ÿญ. React ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ํ™•์ธํ•˜๊ธฐ

1-1) ์ƒˆ๋กœ์šด React ํ”„๋กœ์ ํŠธ ์ƒ์„ฑํ•˜๊ธฐ

โžฐ package.json ํŒŒ์ผ์„ ํ™•์ธํ•˜๋ฉด dependencies ์•ˆ์— @testing์ด๋ผ๋Š” ์ ‘๋‘์–ด๊ฐ€ ๋ถ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

    ใ„ด @testing-library/jest-dom : jest-dom์—์„œ ์ œ๊ณตํ•˜๋Š” custom matcher๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

    ใ„ด @testing-library/react : ์ปดํฌ๋„ŒํŠธ์˜ ์š”์†Œ๋ฅผ ์ฐพ๊ธฐ ์œ„ํ•œ query๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

    ใ„ด @testing-library/user-event : click ๋“ฑ ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ์— ์ด์šฉ๋ฉ๋‹ˆ๋‹ค.

 

1-2) ํ…Œ์ŠคํŠธ ํŒŒ์ผ ํ™•์ธํ•˜๊ธฐ

โžฐ ๋‹ค์‹œ src ํด๋” ์•ˆ์„ ํ™•์ธํ•ด ๋ณด๋ฉด, setupTests.js ์™€ App.test.js ๋ผ๋Š” ์ด๋ฆ„์˜ ํŒŒ์ผ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ณ , App.test.js ํŒŒ์ผ์—๋Š” ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ด๋ฏธ ๋งŒ๋“ค์–ด์ ธ ์žˆ๋‹ค.

โžฐ test ํ•จ์ˆ˜๋Š” Jest ํ•จ์ˆ˜๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ ์ด์šฉํ•˜๋Š” ํ•จ์ˆ˜๋กœ, ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์–ด๋–ค ๋‚ด์šฉ์ธ์ง€ ๋‚˜์ค‘์— ๋‹ค์‹œ ์ฝ์–ด๋„ ํ…Œ์ŠคํŠธ ๋‚ด์šฉ์„ ์•Œ ์ˆ˜ ์žˆ๋Š” ์„ค๋ช…์„ ์ž‘์„ฑํ•˜๊ณ  ๋‘ ๋ฒˆ์งธ ์ธ์ž๋Š” ํ•˜๊ณ ์ž ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ•จ์ˆ˜์˜ ํ˜•ํƒœ๋กœ ๋„ฃ๋Š”๋‹ค.

import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
    render(<App />);
    // ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ render()ํ•จ์ˆ˜๋กœ ์ „๋‹ฌ
    // eact-testing-library์—์„œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ render()ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ์ „๋‹ฌํ•œ๋‹ค.
    const linkElement = screen.getByText(/learn react/i);
    // ender()์—์„œ ๊ฐ€์ ธ์˜จ App ์ปดํฌ๋„ŒํŠธ ์ค‘ "learn react"๋ผ๋Š” ๋ฌธ์ž์—ด์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์—ฌ linkElement์— ํ• ๋‹นํ•œ๋‹ค.
    expect(linkElement).toBeInTheDocument();
    // expect ํ•จ์ˆ˜์˜ ์ธ์ž๋กœ ์ง€์ •ํ•œ ์š”์†Œ๊ฐ€ document.body์— ์กด์žฌํ•˜๋Š”์ง€ toBeInTheDocument ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฒดํฌํ•œ๋‹ค.
    // toBeInTheDocument ํ•จ์ˆ˜๋Š” matchers ํ•จ์ˆ˜๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.
});

โžฐ test ํ•จ์ˆ˜, expect ํ•จ์ˆ˜๋Š” Jest์˜ ํ•จ์ˆ˜๊ณ , toBeInTheDocument๋Š” jest-dom ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ํฌํ•จ๋œ Custom matchers์ด๋‹ค.

โžฐ ์•„์ง jest-dom์„ import ํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ๋„ toBeInTheDocument๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ด์œ ๋Š” src ํด๋”์˜ setupTests.js ํŒŒ์ผ ๋‚ด์—์„œ import ๋˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

1-3) ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์ง์ ‘ ๋งŒ๋“ค๊ธฐ

โžฐ ํŒŒ์ผ๋ช…์„ <ํŒŒ์ผ๋ช…>.test.js๋กœ ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด, Jest๊ฐ€ ํ…Œ์ŠคํŠธ ํŒŒ์ผ๋กœ ํŒ๋‹จํ•ด์„œ ์ž‘๋™ํ•œ๋‹ค.

โžฐ ๋ง์…ˆ์˜ ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ

test('2 ๋”ํ•˜๊ธฐ 2๋Š” 4', () => {
    expect(2 + 2).toBe(4);
});

// toBe ํ•จ์ˆ˜๋Š” matchers ํ•จ์ˆ˜ ์ค‘ ํ•˜๋‚˜๋กœ expect ํ•จ์ˆ˜์— ์ง€์ •ํ•œ ๊ฐ’์ด toBe ํ•จ์ˆ˜์— ์ง€์ •ํ•œ ๊ฐ’๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ์ฒดํฌํ•œ๋‹ค.

โžฐ npm run test๋ฅผ ์‹คํ–‰ํ•˜์—ฌ "a"๋ฅผ ์„ ํƒํ•˜๋ฉด Example.test.js์™€ App.test.js๊ฐ€ ๋ชจ๋‘ ์‹คํ–‰์ด ๋œ๋‹ค.

 

describe('๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ๋“ค', () => {
    test('2 ๋”ํ•˜๊ธฐ 2๋Š” 4', () => {
        expect(2 + 2).toBe(4);
    });
    
    text('2 ๋นผ๊ธฐ 1์€ 1', () => {
        expect(2 - 1).toBe(1);
    });
});

// test ํ•จ์ˆ˜ ๋Œ€์‹  it ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ค๋ฉฐ
// describeํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด itํ•จ์ˆ˜๋‚˜ testํ•จ์ˆ˜๋ฅผ ํ•˜๋‚˜์˜ ํŒŒ์ผ์— ์—ฌ๋Ÿฌ ๊ฐœ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค.

โžฐ describe ํ•จ์ˆ˜ ๋ธ”๋ก์€ Test Suites๋ผ๊ณ  ๋ถˆ๋ฆฌ๋ฉฐ test/it ํ•จ์ˆ˜ ๋ธ”๋ก์€ Test(Test Case)๋ผ๊ณ  ํ•œ๋‹ค.

 

๐Ÿฎ. ์ง์ ‘ ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

2-1) ์ปดํฌ๋„ŒํŠธ ๋งŒ๋“ค๊ธฐ

import {useState} from 'react';

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>
	);
}

export default Light;

// ์ „์›์˜ ์ƒํƒœ๋ฅผ OFF์—์„œ ON์œผ๋กœ ์ „ํ™˜ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ

 

2-2) ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

// Light.test.js

import { render, screen } from '@testing-library/react';
import Light from './Light';

it('renders Light Component', () => {
	render(<Light name="์ „์›" />);
	const nameElement = screen.getByText(/์ „์› off/i);
    // props๋กœ ์ „๋‹ฌ๋œ ์ „์›์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ‘œ์‹œ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ
	expect(nameElement).toBeInTheDocument();
})

------------------------------------------------------------------------

it('off button disabled', () => {
	render(<Light name="์ „์›" />);
	const offButtonElement = screen.getByRole('button', { name: 'OFF' });
	expect(offButtonElement).toBeDisabled();
    // ํ˜„์žฌ OFF๋ฒ„ํŠผ์ด disabled ์ƒํƒœ๋ผ๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ
})

------------------------------------------------------------------------

it('on button enable', () => {
  render(<Light name="์ „์›" />);
  const onButtonElement = screen.getByRole('button', { name: 'ON' });
  expect(onButtonElement).not.toBeDisabled();
  // ON ๋ฒ„ํŠผ์ด disabled๊ฐ€ ์•„๋‹ˆ๋ผ๋Š” ๊ฒƒ์„ ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ
});

------------------------------------------------------------------------

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();
})