Ver3.0 ๐งช JavaScript ํ ์คํ ์๋ฒฝ ๊ฐ์ด๋: Jest์ Mocha๋ก ์์ํ๋ ๋จ์ ํ ์คํธ์ ๋ชจ๋ ๊ฒ

๐งช JavaScript ํ ์คํ ์๋ฒฝ ๊ฐ์ด๋: Jest์ Mocha๋ก ์์ํ๋ ๋จ์ ํ ์คํธ์ ๋ชจ๋ ๊ฒ
์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ๋ฐฐ์ฐ๋ ํ ์คํธ ์ฝ๋ ์์ฑ๋ฒ ๐ก
๐ฏ ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ ๊น?
์๋
! ์ค๋์ JavaScript ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ํ
์คํ
์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ๐
์์งํ ๋งํ๋ฉด, ์ฒ์ ํ๋ก๊ทธ๋๋ฐ์ ๋ฐฐ์ธ ๋๋ "ํ
์คํธ ์ฝ๋? ๊ทธ๊ฑฐ ์ ์จ์ผ ํด? ๊ทธ๋ฅ ์คํํด๋ณด๋ฉด ๋์์!"๋ผ๊ณ ์๊ฐํ๋ ์ ์์ง ์์? ๋๋ ๊ทธ๋ฌ์ด. ๊ทผ๋ฐ ํ๋ก์ ํธ๊ฐ ์ปค์ง๊ณ , ํ์
์ ํ๊ฒ ๋๋ฉด์ ํ
์คํธ ์ฝ๋์ ์ค์์ฑ์ ๋ผ์ ๋ฆฌ๊ฒ ๋๋ผ๊ฒ ๋์ง.
ํ
์คํธ ์ฝ๋๋ ๋ง์น ์์ ๋ฒจํธ ๊ฐ์ ๊ฑฐ์ผ. ๋น์ฅ์ ๋ถํธํ๊ณ ๊ท์ฐฎ์ ๋ณด์ด์ง๋ง, ์ํํ ์๊ฐ์ด ์์ ๋ ๋๋ฅผ ์ง์ผ์ฃผ๋ ์๋ช
์ค์ด ๋๊ฑฐ๋ . ๐๐จ
1. ๋ฆฌํฉํ ๋งํ ๋
์ฝ๋๋ฅผ ๊ฐ์ ํ๋ ค๊ณ ์์ ํ๋๋ฐ, ๋ค๋ฅธ ๋ถ๋ถ์ด ๋ง๊ฐ์ก๋์ง ํ์ธํ๊ธฐ ์ํด ๋ชจ๋ ๊ธฐ๋ฅ์ ์๋์ผ๋ก ํ ์คํธํ๋ ๊ฑด ์ ๋ง ๋นํจ์จ์ ์ด์ผ.
2. ํ์ ํ ๋
๋๋ฃ๊ฐ ์์ฑํ ์ฝ๋๋ฅผ ์์ ํด์ผ ํ๋๋ฐ, ๋ด๊ฐ ๋ญ ๊ฑด๋๋ฆฌ๋ฉด ์ ๋๋์ง ์ ์๊ฐ ์์ด. ํ ์คํธ ์ฝ๋๊ฐ ์์ผ๋ฉด ์์ ํ๊ฒ ์์ ํ ์ ์์ง.
3. ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐํ ๋
๊ธฐ์กด ๊ธฐ๋ฅ์ด ์ ์์ ์ผ๋ก ์๋ํ๋์ง ํ์ธํ๋ฉด์ ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด.
4. ๋ฒ๊ทธ ์์ ํ ๋
๋ฒ๊ทธ๋ฅผ ์ฌํํ๋ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํ๊ณ , ๊ทธ ํ ์คํธ๊ฐ ํต๊ณผํ๋๋ก ์ฝ๋๋ฅผ ์์ ํ๋ฉด ๊ฐ์ ๋ฒ๊ทธ๊ฐ ๋ค์ ๋ฐ์ํ์ง ์์.
๐ ๋จ์ ํ ์คํธ(Unit Test)๋?
ํ
์คํธ์๋ ์ฌ๋ฌ ์ข
๋ฅ๊ฐ ์์ด. ๋จ์ ํ
์คํธ(Unit Test), ํตํฉ ํ
์คํธ(Integration Test), E2E ํ
์คํธ(End-to-End Test) ๋ฑ๋ฑ. ๊ทธ์ค์์ ๋จ์ ํ
์คํธ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ด ๋๋ ํ
์คํธ์ผ.
๋จ์ ํ
์คํธ๋ ๋ง ๊ทธ๋๋ก ์ฝ๋์ ๊ฐ์ฅ ์์ ๋จ์(์ฃผ๋ก ํจ์๋ ๋ฉ์๋)๋ฅผ ํ
์คํธํ๋ ๊ฑฐ์ผ. ์๋ฅผ ๋ค์ด, ๋ ์ซ์๋ฅผ ๋ํ๋ ํจ์๊ฐ ์๋ค๋ฉด, ๊ทธ ํจ์์ ์ฌ๋ฌ ์
๋ ฅ๊ฐ์ ๋ฃ์ด๋ณด๊ณ ์์ํ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋์ง ํ์ธํ๋ ๊ฑฐ์ง.
โ ๋น ๋ฅด๋ค
๊ฐ ํ ์คํธ๊ฐ ๋ ๋ฆฝ์ ์ด๊ณ ๊ฐ๋จํด์ ์คํ ์๋๊ฐ ๋น ๋ฅด๋ค.
โ ๋ ๋ฆฝ์ ์ด๋ค
๋ค๋ฅธ ํ ์คํธ๋ ์ธ๋ถ ์์คํ ์ ์์กดํ์ง ์๋๋ค.
โ ๋ฐ๋ณต ๊ฐ๋ฅํ๋ค
์ธ์ ์คํํด๋ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์จ๋ค.
โ ๋ช ํํ๋ค
๋ฌด์์ ํ ์คํธํ๋์ง, ์ ์คํจํ๋์ง ์ฝ๊ฒ ์ ์ ์๋ค.
๊ฐ๋จํ ์์๋ฅผ ๋ณผ๊น? ๐ค
// ํ
์คํธํ ํจ์
function add(a, b) {
return a + b;
}
// ๋จ์ ํ
์คํธ (๊ฐ๋
์ ์์)
ํ
์คํธ: "add ํจ์๋ ๋ ์ซ์๋ฅผ ๋ํ๋ค"
์
๋ ฅ: add(2, 3)
์์ ๊ฒฐ๊ณผ: 5
์ค์ ๊ฒฐ๊ณผ: 5
โ
ํต๊ณผ!
ํ
์คํธ: "add ํจ์๋ ์์๋ ์ฒ๋ฆฌํ๋ค"
์
๋ ฅ: add(-1, 1)
์์ ๊ฒฐ๊ณผ: 0
์ค์ ๊ฒฐ๊ณผ: 0
โ
ํต๊ณผ!
์ด๋ ๊ฒ ๊ฐ ํจ์์ ๋์์ ์ธ๋ฐํ๊ฒ ๊ฒ์ฆํ๋ ๊ฒ ๋จ์ ํ ์คํธ์ผ. ๊ฐ๋จํ์ง? ๐
๐ญ Jest vs Mocha: ์ด๋ค ๊ฑธ ์ ํํด์ผ ํ ๊น?
JavaScript ํ
์คํ
์ธ๊ณ์๋ ์ฌ๋ฌ ํ๋ ์์ํฌ๊ฐ ์์ง๋ง, Jest์ Mocha๊ฐ ๊ฐ์ฅ ์ธ๊ธฐ ์์ด. ๋ ๋ค ํ๋ฅญํ ๋๊ตฌ์ธ๋ฐ, ๊ฐ๊ฐ์ ํน์ง์ด ๋ฌ๋ผ์ ํ๋ก์ ํธ ์ํฉ์ ๋ง๊ฒ ์ ํํ๋ฉด ๋ผ.
๋จผ์ ๊ฐ๊ฐ์ ํน์ง์ ์ดํด๋ณด์! ๐
๐ Jest
๊ฐ๋ฐ์ฌ: Facebook(Meta)
ํ์: 2014๋
์ฒ ํ: "Zero Configuration"
์ฅ์ :
โข ์ค์ ์ด ๊ฑฐ์ ํ์ ์์ (๋ฐ๋ก ์์ ๊ฐ๋ฅ)
โข ๋ด์ฅ๋ assertion ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โข ๋ด์ฅ๋ mocking ๊ธฐ๋ฅ
โข ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ์๋ ์์ฑ
โข ์ค๋
์ท ํ
์คํ
์ง์
โข React์ ์๋ฒฝํ ํธํ
โข ๋ณ๋ ฌ ํ
์คํธ ์คํ์ผ๋ก ๋น ๋ฅธ ์๋
๋จ์ :
โข ์๋์ ์ผ๋ก ๋ฌด๊ฑฐ์
โข ์ปค์คํฐ๋ง์ด์ง ์ ํ์
โข ํ์ต ๊ณก์ ์ด ์์ (๋ง์ ๊ธฐ๋ฅ)
โ Mocha
๊ฐ๋ฐ์ฌ: ์ปค๋ฎค๋ํฐ ์ฃผ๋
ํ์: 2011๋
์ฒ ํ: "์ ์ฐ์ฑ๊ณผ ํ์ฅ์ฑ"
์ฅ์ :
โข ๋งค์ฐ ์ ์ฐํ๊ณ ํ์ฅ ๊ฐ๋ฅ
โข ๊ฐ๋ณ๊ณ ๋น ๋ฆ
โข ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์กฐํฉ ๊ฐ๋ฅ
โข ์ค๋๋ ๋งํผ ์์ ์
โข ๋ธ๋ผ์ฐ์ ์ Node.js ๋ชจ๋ ์ง์
โข ๋น๋๊ธฐ ํ
์คํธ์ ๊ฐํจ
๋จ์ :
โข ์ด๊ธฐ ์ค์ ์ด ๋ณต์กํจ
โข assertion, mocking ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ณ๋ ํ์
โข ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๋๊ตฌ ๋ณ๋ ์ค์น
โข ์ค์ ์กฐํฉ์ด ๋ง์ ํผ๋์ค๋ฌ์ธ ์ ์์
Jest๋ฅผ ์ ํํ๋ฉด ์ข์ ๊ฒฝ์ฐ:
โข React, Vue ๋ฑ ๋ชจ๋ ํ๋ ์์ํฌ ํ๋ก์ ํธ
โข ๋น ๋ฅด๊ฒ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถํ๊ณ ์ถ์ ๋
โข ํ์๋ค์ด ํ ์คํ ์ด๋ณด์์ผ ๋
โข ์ค๋ ์ท ํ ์คํ ์ด ํ์ํ ๋
Mocha๋ฅผ ์ ํํ๋ฉด ์ข์ ๊ฒฝ์ฐ:
โข ๋ ๊ฑฐ์ ํ๋ก์ ํธ๋ ํน์ํ ์๊ตฌ์ฌํญ์ด ์์ ๋
โข ํ ์คํ ํ๊ฒฝ์ ์ธ๋ฐํ๊ฒ ์ปค์คํฐ๋ง์ด์งํ๊ณ ์ถ์ ๋
โข ๊ฐ๋ฒผ์ด ํ ์คํธ ๋ฌ๋๊ฐ ํ์ํ ๋
โข ์ด๋ฏธ Mocha ์ํ๊ณ์ ์ต์ํ ํ์ผ ๋
๐ Jest ์์ํ๊ธฐ: ์ค์น๋ถํฐ ์ฒซ ํ ์คํธ๊น์ง
์, ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก Jest๋ฅผ ์ฌ์ฉํด๋ณด์! Jest๋ ์ ๋ง ์์ํ๊ธฐ ์ฌ์. ๐
ํ๋ก์ ํธ ํด๋์์ ํฐ๋ฏธ๋์ ์ด๊ณ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด:
# npm์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
npm install --save-dev jest
# yarn์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
yarn add --dev jest
--save-dev ์ต์ ์ ๊ฐ๋ฐ ์์กด์ฑ์ผ๋ก ์ค์นํ๋ค๋ ๋ป์ด์ผ. ํ ์คํธ ๋๊ตฌ๋ ์ค์ ๋ฐฐํฌํ ๋๋ ํ์ ์์ผ๋๊น!
package.json ํ์ผ์ ์ด์ด์ scripts ๋ถ๋ถ์ ํ ์คํธ ๋ช ๋ น์ด๋ฅผ ์ถ๊ฐํด:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
์ด์ npm test ๋ช ๋ น์ด๋ก ํ ์คํธ๋ฅผ ์คํํ ์ ์์ด! ๐
๊ฐ๋จํ ํจ์์ ํ ์คํธ๋ฅผ ์์ฑํด๋ณด์. ๋จผ์ ํ ์คํธํ ํจ์๋ฅผ ๋ง๋ค์ด:
// math.js
function sum(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('0์ผ๋ก ๋๋ ์ ์์ต๋๋ค!');
}
return a / b;
}
module.exports = { sum, subtract, multiply, divide };
์ด์ ํ ์คํธ ํ์ผ์ ๋ง๋ค์ด. Jest๋ .test.js ๋๋ .spec.js ํ์ฅ์๋ฅผ ๊ฐ์ง ํ์ผ์ ์๋์ผ๋ก ์ฐพ์:
// math.test.js
const { sum, subtract, multiply, divide } = require('./math');
describe('์ํ ํจ์ ํ
์คํธ', () => {
test('2 + 3์ 5์ด๋ค', () => {
expect(sum(2, 3)).toBe(5);
});
test('5 - 2๋ 3์ด๋ค', () => {
expect(subtract(5, 2)).toBe(3);
});
test('4 * 3์ 12์ด๋ค', () => {
expect(multiply(4, 3)).toBe(12);
});
test('10 / 2๋ 5์ด๋ค', () => {
expect(divide(10, 2)).toBe(5);
});
test('0์ผ๋ก ๋๋๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค', () => {
expect(() => divide(10, 0)).toThrow('0์ผ๋ก ๋๋ ์ ์์ต๋๋ค!');
});
});
describe(): ํ ์คํธ ๊ทธ๋ฃน์ ๋ง๋ค์ด. ๊ด๋ จ๋ ํ ์คํธ๋ค์ ๋ฌถ์ด์ ๊ด๋ฆฌํ ์ ์์ด.
test() ๋๋ it(): ๊ฐ๋ณ ํ ์คํธ ์ผ์ด์ค๋ฅผ ์ ์ํด. ๋์ ์์ ํ ๊ฐ์ ๊ธฐ๋ฅ์ด์ผ!
expect(): ํ ์คํธํ ๊ฐ์ ์ ๋ฌํด.
toBe(): ์์ ๊ฒฐ๊ณผ์ ๋น๊ตํด. ์ด๋ฐ ๊ฑธ "matcher"๋ผ๊ณ ๋ถ๋ฌ.
toThrow(): ํจ์๊ฐ ์๋ฌ๋ฅผ ๋์ง๋์ง ํ์ธํด.
ํฐ๋ฏธ๋์์ ๋ค์ ๋ช ๋ น์ด๋ฅผ ์ ๋ ฅํด:
npm test
๊ทธ๋ฌ๋ฉด ์ด๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์ฌ ๊ฑฐ์ผ:
PASS ./math.test.js
์ํ ํจ์ ํ
์คํธ
โ 2 + 3์ 5์ด๋ค (3ms)
โ 5 - 2๋ 3์ด๋ค (1ms)
โ 4 * 3์ 12์ด๋ค
โ 10 / 2๋ 5์ด๋ค
โ 0์ผ๋ก ๋๋๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค (2ms)
Test Suites: 1 passed, 1 total
Tests: 5 passed, 5 total
Snapshots: 0 total
Time: 1.234s
๋ชจ๋ ํ ์คํธ๊ฐ ํต๊ณผํ์ด! ๐ ์ด๋ก์ ์ฒดํฌ ๋งํฌ๊ฐ ๋ณด์ด๋ฉด ์ฑ๊ณต์ด์ผ.
๐จ Jest์ ์ฃผ์ Matcher ์์๋ณด๊ธฐ
Jest๋ ๋ค์ํ matcher๋ฅผ ์ ๊ณตํด์ ์ฌ๋ฌ ์ํฉ์ ํ ์คํธํ ์ ์์ด. ์์ฃผ ์ฌ์ฉํ๋ matcher๋ค์ ์์๋ณด์! ๐
| Matcher | ์ค๋ช | ์์ |
|---|---|---|
| toBe() | ์ ํํ ๊ฐ์์ง (===) | expect(2+2).toBe(4) |
| toEqual() | ๊ฐ์ด ๊ฐ์์ง (๊ฐ์ฒด ๋น๊ต) | expect({a:1}).toEqual({a:1}) |
| toBeTruthy() | ์ฐธ์ธ์ง | expect(true).toBeTruthy() |
| toBeFalsy() | ๊ฑฐ์ง์ธ์ง | expect(false).toBeFalsy() |
| toBeNull() | null์ธ์ง | expect(null).toBeNull() |
| toBeUndefined() | undefined์ธ์ง | expect(undefined).toBeUndefined() |
| toContain() | ๋ฐฐ์ด/๋ฌธ์์ด์ ํฌํจ๋๋์ง | expect([1,2,3]).toContain(2) |
| toHaveLength() | ๊ธธ์ด๊ฐ ๋ง๋์ง | expect([1,2]).toHaveLength(2) |
| toThrow() | ์๋ฌ๋ฅผ ๋์ง๋์ง | expect(()=>error()).toThrow() |
| toBeGreaterThan() | ๋ ํฐ์ง | expect(10).toBeGreaterThan(5) |
| toBeLessThan() | ๋ ์์์ง | expect(5).toBeLessThan(10) |
| toMatch() | ์ ๊ท์ ๋งค์นญ | expect('hello').toMatch(/ell/) |
์ค์ ์ฌ์ฉ ์์๋ฅผ ๋ ๋ณผ๊น? ๐ค
// user.test.js
describe('์ฌ์ฉ์ ๊ฐ์ฒด ํ
์คํธ', () => {
const user = {
name: '๊น์ฝ๋ฉ',
age: 25,
email: 'coding@example.com',
hobbies: ['๋
์', '์ฝ๋ฉ', '์ด๋']
};
test('์ฌ์ฉ์ ์ด๋ฆ์ด ์ฌ๋ฐ๋ฅด๋ค', () => {
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
test('์ฌ์ฉ์ ๊ฐ์ฒด๊ฐ ์ฌ๋ฐ๋ฅธ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ง๋ค', () => {
expect(user).toEqual({
name: '๊น์ฝ๋ฉ',
age: 25,
email: 'coding@example.com',
hobbies: ['๋
์', '์ฝ๋ฉ', '์ด๋']
});
});
test('๋์ด๊ฐ 20๋ณด๋ค ํฌ๋ค', () => {
expect(user.age).toBeGreaterThan(20);
});
test('์ทจ๋ฏธ ๋ฐฐ์ด์ "์ฝ๋ฉ"์ด ํฌํจ๋๋ค', () => {
expect(user.hobbies).toContain('์ฝ๋ฉ');
});
test('์ทจ๋ฏธ๊ฐ 3๊ฐ๋ค', () => {
expect(user.hobbies).toHaveLength(3);
});
test('์ด๋ฉ์ผ ํ์์ด ์ฌ๋ฐ๋ฅด๋ค', () => {
expect(user.email).toMatch(/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/);
});
});
๋ชจ๋ matcher ์์ .not์ ๋ถ์ด๋ฉด ๋ฐ๋๋ก ํ ์คํธํ ์ ์์ด!
expect(user.age).not.toBe(30);
expect(user.hobbies).not.toContain('๊ฒ์');
โฑ๏ธ ๋น๋๊ธฐ ์ฝ๋ ํ ์คํธํ๊ธฐ
์ค์ ๊ฐ๋ฐ์์๋ API ํธ์ถ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๊ฐ์ ๋น๋๊ธฐ ์์ ์ด ๋ง์. Jest์์ ๋น๋๊ธฐ ์ฝ๋๋ฅผ ํ ์คํธํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์! ๐
// api.js
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, name: '๊น์ฝ๋ฉ' });
} else {
reject(new Error('์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค'));
}
}, 100);
});
}
module.exports = { fetchUser };
// api.test.js
const { fetchUser } = require('./api');
describe('API ํ
์คํธ', () => {
// ๋ฐฉ๋ฒ 1: return ์ฌ์ฉ
test('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค (return)', () => {
return fetchUser(1).then(user => {
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
});
// ๋ฐฉ๋ฒ 2: async/await ์ฌ์ฉ (์ถ์ฒ!)
test('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค (async/await)', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
// ์๋ฌ ํ
์คํธ
test('์กด์ฌํ์ง ์๋ ์ฌ์ฉ์๋ ์๋ฌ๋ฅผ ๋์ง๋ค', async () => {
await expect(fetchUser(999)).rejects.toThrow('์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
});
// resolves matcher ์ฌ์ฉ
test('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค (resolves)', async () => {
await expect(fetchUser(1)).resolves.toEqual({
id: 1,
name: '๊น์ฝ๋ฉ'
});
});
});
๋น๋๊ธฐ ํ ์คํธ๋ฅผ ์์ฑํ ๋๋ ๋ฐ๋์:
โข return์ ์ฌ์ฉํ๊ฑฐ๋
โข async/await์ ์ฌ์ฉํด์ผ ํด!
๊ทธ๋ ์ง ์์ผ๋ฉด Jest๊ฐ Promise๊ฐ ์๋ฃ๋๊ธฐ ์ ์ ํ ์คํธ๋ฅผ ๋๋ด๋ฒ๋ ค์ ํญ์ ํต๊ณผํ๋ ๊ฒ์ฒ๋ผ ๋ณด์ผ ์ ์์ด. ๐ฑ
setTimeout์ด๋ setInterval์ ์ฌ์ฉํ๋ ์ฝ๋๋ ์ด๋ป๊ฒ ํ ์คํธํ ๊น? Jest์ ํ์ด๋จธ ๋ชจํน ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ฉด ๋ผ!
// timer.js
function delayedGreeting(name, callback) {
setTimeout(() => {
callback(`์๋
ํ์ธ์, ${name}๋!`);
}, 1000);
}
module.exports = { delayedGreeting };
// timer.test.js
const { delayedGreeting } = require('./timer');
describe('ํ์ด๋จธ ํ
์คํธ', () => {
beforeEach(() => {
jest.useFakeTimers(); // ๊ฐ์ง ํ์ด๋จธ ์ฌ์ฉ
});
afterEach(() => {
jest.useRealTimers(); // ์ค์ ํ์ด๋จธ๋ก ๋ณต์
});
test('1์ด ํ์ ์ธ์ฌ ๋ฉ์์ง๋ฅผ ๋ณด๋ธ๋ค', () => {
const callback = jest.fn(); // ๋ชจํน๋ ํจ์
delayedGreeting('๊น์ฝ๋ฉ', callback);
// ์์ง ์คํ ์ ๋จ
expect(callback).not.toHaveBeenCalled();
// 1์ด ๊ฒฝ๊ณผ
jest.advanceTimersByTime(1000);
// ์ด์ ์คํ๋จ!
expect(callback).toHaveBeenCalledWith('์๋
ํ์ธ์, ๊น์ฝ๋ฉ๋!');
expect(callback).toHaveBeenCalledTimes(1);
});
});
jest.useFakeTimers()๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์ ๋ก 1์ด๋ฅผ ๊ธฐ๋ค๋ฆด ํ์ ์์ด jest.advanceTimersByTime()์ผ๋ก ์๊ฐ์ ๊ฑด๋๋ธ ์ ์์ด! ํ ์คํธ๊ฐ ์์ฒญ ๋นจ๋ผ์ง์ง. โก
๐ญ Mock ํจ์์ Spy ํ์ฉํ๊ธฐ
ํ ์คํธ๋ฅผ ์์ฑํ๋ค ๋ณด๋ฉด ์ธ๋ถ ์์กด์ฑ(API, ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋ค๋ฅธ ๋ชจ๋ ๋ฑ)์ ๊ฐ์ง ์ฝ๋๋ฅผ ๋ง๋๊ฒ ๋ผ. ์ด๋ด ๋ Mock์ ์ฌ์ฉํ๋ฉด ์ค์ ์์กด์ฑ ์์ด๋ ํ ์คํธํ ์ ์์ด! ๐ช
describe('Mock ํจ์ ๊ธฐ๋ณธ', () => {
test('Mock ํจ์๋ ํธ์ถ์ ์ถ์ ํ๋ค', () => {
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
// ํธ์ถ ํ์ ํ์ธ
expect(mockFn).toHaveBeenCalledTimes(2);
// ํน์ ์ธ์๋ก ํธ์ถ๋์๋์ง ํ์ธ
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenCalledWith('world');
});
test('Mock ํจ์๋ ๋ฐํ๊ฐ์ ์ค์ ํ ์ ์๋ค', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue('๋ชจํน๋ ๊ฐ');
expect(mockFn()).toBe('๋ชจํน๋ ๊ฐ');
});
test('Mock ํจ์๋ ์ฌ๋ฌ ๋ฐํ๊ฐ์ ์ค์ ํ ์ ์๋ค', () => {
const mockFn = jest.fn();
mockFn
.mockReturnValueOnce('์ฒซ ๋ฒ์งธ')
.mockReturnValueOnce('๋ ๋ฒ์งธ')
.mockReturnValue('๊ธฐ๋ณธ๊ฐ');
expect(mockFn()).toBe('์ฒซ ๋ฒ์งธ');
expect(mockFn()).toBe('๋ ๋ฒ์งธ');
expect(mockFn()).toBe('๊ธฐ๋ณธ๊ฐ');
expect(mockFn()).toBe('๊ธฐ๋ณธ๊ฐ');
});
test('Mock ํจ์๋ ๊ตฌํ์ ์ ๊ณตํ ์ ์๋ค', () => {
const mockFn = jest.fn(x => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
});
});
์ค์ ํ๋ก์ ํธ์์๋ ์ธ๋ถ ๋ชจ๋์ ๋ชจํนํด์ผ ํ ๋๊ฐ ๋ง์. ์๋ฅผ ๋ค์ด, axios ๊ฐ์ HTTP ํด๋ผ์ด์ธํธ๋ฅผ ๋ชจํนํด๋ณด์:
// userService.js
const axios = require('axios');
async function getUserById(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
async function createUser(userData) {
const response = await axios.post('https://api.example.com/users', userData);
return response.data;
}
module.exports = { getUserById, createUser };
// userService.test.js
const axios = require('axios');
const { getUserById, createUser } = require('./userService');
// axios ๋ชจ๋ ์ ์ฒด๋ฅผ ๋ชจํน
jest.mock('axios');
describe('์ฌ์ฉ์ ์๋น์ค ํ
์คํธ', () => {
afterEach(() => {
jest.clearAllMocks(); // ๊ฐ ํ
์คํธ ํ ๋ชจํน ์ด๊ธฐํ
});
test('์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค', async () => {
const mockUser = { id: 1, name: '๊น์ฝ๋ฉ', email: 'test@example.com' };
// axios.get์ด ๋ฐํํ ๊ฐ ์ค์
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUserById(1);
expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(axios.get).toHaveBeenCalledTimes(1);
});
test('์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', async () => {
const newUser = { name: '๋ฐ๊ฐ๋ฐ', email: 'dev@example.com' };
const createdUser = { id: 2, ...newUser };
axios.post.mockResolvedValue({ data: createdUser });
const result = await createUser(newUser);
expect(result).toEqual(createdUser);
expect(axios.post).toHaveBeenCalledWith(
'https://api.example.com/users',
newUser
);
});
test('API ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ค', async () => {
axios.get.mockRejectedValue(new Error('๋คํธ์ํฌ ์๋ฌ'));
await expect(getUserById(1)).rejects.toThrow('๋คํธ์ํฌ ์๋ฌ');
});
});
1. ๋ ๋ฆฝ์ฑ
์ค์ API๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ด๋ ํ ์คํธ ๊ฐ๋ฅ
2. ์๋
๋คํธ์ํฌ ์์ฒญ์ ๊ธฐ๋ค๋ฆด ํ์ ์์ด์ ๋น ๋ฆ
3. ์์ ์ฑ
์ธ๋ถ ์๋น์ค์ ์ํ์ ์ํฅ๋ฐ์ง ์์
4. ์ ์ด
์๋ฌ ์ํฉ์ด๋ ํน์ํ ์ผ์ด์ค๋ฅผ ์ฝ๊ฒ ์ฌํ ๊ฐ๋ฅ
โ Mocha ์์ํ๊ธฐ: ์ ์ฐํ ํ ์คํ ์ ์ธ๊ณ
์ด์ Mocha๋ฅผ ์์๋ณผ ์ฐจ๋ก์ผ! Mocha๋ Jest๋ณด๋ค ์ค์ ์ด ์ข ๋ ํ์ํ์ง๋ง, ๊ทธ๋งํผ ์์ ๋๊ฐ ๋์. ๐จ
Mocha๋ assertion ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ด์ฅ๋์ด ์์ง ์์์ ๋ณ๋๋ก ์ค์นํด์ผ ํด. ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์กฐํฉ์ Mocha + Chai์ผ:
# Mocha์ Chai ์ค์น
npm install --save-dev mocha chai
# ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ํ nyc ์ค์น (์ ํ)
npm install --save-dev nyc
package.json์ ์คํฌ๋ฆฝํธ ์ถ๊ฐ:
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch",
"test:coverage": "nyc mocha"
}
}
// calculator.js
class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
multiply(a, b) {
return a * b;
}
divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}
module.exports = Calculator;
// calculator.test.js
const { expect } = require('chai');
const Calculator = require('./calculator');
describe('Calculator', () => {
let calculator;
// ๊ฐ ํ
์คํธ ์ ์ ์คํ
beforeEach(() => {
calculator = new Calculator();
});
describe('add()', () => {
it('๋ ์ซ์๋ฅผ ๋ํ๋ค', () => {
const result = calculator.add(2, 3);
expect(result).to.equal(5);
});
it('์์๋ ์ฒ๋ฆฌํ๋ค', () => {
expect(calculator.add(-1, -2)).to.equal(-3);
});
});
describe('subtract()', () => {
it('๋ ์ซ์๋ฅผ ๋บ๋ค', () => {
expect(calculator.subtract(5, 3)).to.equal(2);
});
});
describe('multiply()', () => {
it('๋ ์ซ์๋ฅผ ๊ณฑํ๋ค', () => {
expect(calculator.multiply(4, 3)).to.equal(12);
});
});
describe('divide()', () => {
it('๋ ์ซ์๋ฅผ ๋๋๋ค', () => {
expect(calculator.divide(10, 2)).to.equal(5);
});
it('0์ผ๋ก ๋๋๋ฉด ์๋ฌ๋ฅผ ๋์ง๋ค', () => {
expect(() => calculator.divide(10, 0)).to.throw('Division by zero');
});
});
});
describe(): ํ ์คํธ ์ค์ํธ(๊ทธ๋ฃน)๋ฅผ ์ ์. ์ค์ฒฉ ๊ฐ๋ฅ!
it(): ๊ฐ๋ณ ํ ์คํธ ์ผ์ด์ค. test()์ ๊ฐ์ ์ญํ
beforeEach(): ๊ฐ ํ ์คํธ ์ ์ ์คํ
afterEach(): ๊ฐ ํ ์คํธ ํ์ ์คํ
before(): ๋ชจ๋ ํ ์คํธ ์ ์ ํ ๋ฒ ์คํ
after(): ๋ชจ๋ ํ ์คํธ ํ์ ํ ๋ฒ ์คํ
Chai๋ ์ธ ๊ฐ์ง assertion ์คํ์ผ์ ์ ๊ณตํด. ์ทจํฅ์ ๋ง๊ฒ ์ ํํ๋ฉด ๋ผ! ๐
const { expect, assert, should } = require('chai');
describe('Chai Assertion ์คํ์ผ', () => {
const value = 42;
const obj = { name: '๊น์ฝ๋ฉ', age: 25 };
const arr = [1, 2, 3];
describe('Expect ์คํ์ผ (๊ฐ์ฅ ์ธ๊ธฐ)', () => {
it('๊ฐ์ ๊ฒ์ฆํ๋ค', () => {
expect(value).to.equal(42);
expect(value).to.be.a('number');
expect(obj).to.have.property('name');
expect(arr).to.include(2);
expect(arr).to.have.lengthOf(3);
});
});
describe('Assert ์คํ์ผ (์ ํต์ )', () => {
it('๊ฐ์ ๊ฒ์ฆํ๋ค', () => {
assert.equal(value, 42);
assert.typeOf(value, 'number');
assert.property(obj, 'name');
assert.include(arr, 2);
assert.lengthOf(arr, 3);
});
});
describe('Should ์คํ์ผ (BDD)', () => {
before(() => {
should(); // should ํ์ฑํ
});
it('๊ฐ์ ๊ฒ์ฆํ๋ค', () => {
value.should.equal(42);
value.should.be.a('number');
obj.should.have.property('name');
arr.should.include(2);
arr.should.have.lengthOf(3);
});
});
});
๊ฐ์ธ์ ์ผ๋ก๋ expect ์คํ์ผ์ ์ถ์ฒํด! ๊ฐ์ฅ ์ฝ๊ธฐ ์ฝ๊ณ ์ง๊ด์ ์ด๊ฑฐ๋ . ๐
๐ Mocha์์ ๋น๋๊ธฐ ํ ์คํธํ๊ธฐ
Mocha๋ ๋น๋๊ธฐ ํ ์คํธ๋ฅผ ํ๋ฅญํ๊ฒ ์ง์ํด. ์ฌ๋ฌ ๋ฐฉ๋ฒ์ด ์๋๋ฐ ํ๋์ฉ ์์๋ณด์! โฑ๏ธ
const { expect } = require('chai');
describe('๋น๋๊ธฐ ํ
์คํธ - Callback', () => {
function fetchData(callback) {
setTimeout(() => {
callback(null, { data: 'success' });
}, 100);
}
it('์ฝ๋ฐฑ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋๋ค', (done) => {
fetchData((err, result) => {
expect(err).to.be.null;
expect(result.data).to.equal('success');
done(); // ํ
์คํธ ์๋ฃ๋ฅผ ์๋ฆผ
});
});
});
describe('๋น๋๊ธฐ ํ
์คํธ - Promise', () => {
function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: '๊น์ฝ๋ฉ' });
}, 100);
});
}
it('Promise๋ฅผ ๋ฐํํ๋ค', () => {
return fetchUser(1).then(user => {
expect(user.name).to.equal('๊น์ฝ๋ฉ');
});
});
});
describe('๋น๋๊ธฐ ํ
์คํธ - Async/Await', () => {
async function fetchUser(id) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: '๊น์ฝ๋ฉ' });
}, 100);
});
}
it('async/await์ผ๋ก ํ
์คํธํ๋ค', async () => {
const user = await fetchUser(1);
expect(user.name).to.equal('๊น์ฝ๋ฉ');
expect(user.id).to.equal(1);
});
it('์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๋ค', async () => {
async function failingFunction() {
throw new Error('์๋ฌ ๋ฐ์!');
}
try {
await failingFunction();
throw new Error('์๋ฌ๊ฐ ๋ฐ์ํด์ผ ํฉ๋๋ค');
} catch (error) {
expect(error.message).to.equal('์๋ฌ ๋ฐ์!');
}
});
});
๊ธฐ๋ณธ์ ์ผ๋ก Mocha๋ 2์ด ์์ ํ ์คํธ๊ฐ ์๋ฃ๋์ง ์์ผ๋ฉด ์คํจ๋ก ์ฒ๋ฆฌํด. ๊ธด ์์ ์ ํ ์คํธํ ๋๋ ํ์์์์ ๋๋ ค์ค์ผ ํด:
it('๊ธด ์์ ', async function() {
this.timeout(5000); // 5์ด๋ก ์ค์
// ํ ์คํธ ์ฝ๋...
});
๐ญ Mocha์์ Mocking: Sinon ํ์ฉํ๊ธฐ
Mocha๋ ๋ชจํน ๊ธฐ๋ฅ์ด ๋ด์ฅ๋์ด ์์ง ์์์ Sinon์ด๋ผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฃผ๋ก ์ฌ์ฉํด. Sinon์ ๋งค์ฐ ๊ฐ๋ ฅํ ๋ชจํน ๋๊ตฌ์ผ! ๐ช
npm install --save-dev sinon
Spy๋ ํจ์ ํธ์ถ์ ๊ฐ์ํ๋ ๋๊ตฌ์ผ. ํจ์๊ฐ ๋ช ๋ฒ ํธ์ถ๋์๋์ง, ์ด๋ค ์ธ์๋ก ํธ์ถ๋์๋์ง ์ถ์ ํ ์ ์์ด.
const { expect } = require('chai');
const sinon = require('sinon');
describe('Sinon Spy', () => {
it('ํจ์ ํธ์ถ์ ์ถ์ ํ๋ค', () => {
const callback = sinon.spy();
function doSomething(cb) {
cb('hello');
cb('world');
}
doSomething(callback);
expect(callback.calledTwice).to.be.true;
expect(callback.firstCall.args[0]).to.equal('hello');
expect(callback.secondCall.args[0]).to.equal('world');
});
it('๊ฐ์ฒด ๋ฉ์๋๋ฅผ ๊ฐ์ํ๋ค', () => {
const user = {
name: '๊น์ฝ๋ฉ',
setName(newName) {
this.name = newName;
}
};
const spy = sinon.spy(user, 'setName');
user.setName('๋ฐ๊ฐ๋ฐ');
expect(spy.calledOnce).to.be.true;
expect(spy.calledWith('๋ฐ๊ฐ๋ฐ')).to.be.true;
expect(user.name).to.equal('๋ฐ๊ฐ๋ฐ');
spy.restore(); // ์๋๋๋ก ๋ณต์
});
});
Stub์ ํจ์์ ๋์์ ์์ ํ ๋์ฒดํ ์ ์์ด. ํน์ ๊ฐ์ ๋ฐํํ๋๋ก ์ค์ ํ๊ฑฐ๋, ์๋ฌ๋ฅผ ๋์ง๋๋ก ๋ง๋ค ์ ์์ง.
const sinon = require('sinon');
const { expect } = require('chai');
describe('Sinon Stub', () => {
it('ํจ์์ ๋ฐํ๊ฐ์ ์กฐ์ํ๋ค', () => {
const stub = sinon.stub();
stub.returns('๋ชจํน๋ ๊ฐ');
expect(stub()).to.equal('๋ชจํน๋ ๊ฐ');
});
it('์ฌ๋ฌ ๋ฐํ๊ฐ์ ์ค์ ํ๋ค', () => {
const stub = sinon.stub();
stub.onFirstCall().returns('์ฒซ ๋ฒ์งธ');
stub.onSecondCall().returns('๋ ๋ฒ์งธ');
stub.returns('๊ธฐ๋ณธ๊ฐ');
expect(stub()).to.equal('์ฒซ ๋ฒ์งธ');
expect(stub()).to.equal('๋ ๋ฒ์งธ');
expect(stub()).to.equal('๊ธฐ๋ณธ๊ฐ');
});
it('์กฐ๊ฑด๋ถ ๋ฐํ๊ฐ์ ์ค์ ํ๋ค', () => {
const stub = sinon.stub();
stub.withArgs('hello').returns('์๋
!');
stub.withArgs('bye').returns('์๊ฐ!');
stub.returns('๋ชจ๋ฅด๋ ๋ง');
expect(stub('hello')).to.equal('์๋
!');
expect(stub('bye')).to.equal('์๊ฐ!');
expect(stub('???')).to.equal('๋ชจ๋ฅด๋ ๋ง');
});
it('์๋ฌ๋ฅผ ๋์ง๋ค', () => {
const stub = sinon.stub();
stub.throws(new Error('์๋ฌ ๋ฐ์!'));
expect(() => stub()).to.throw('์๋ฌ ๋ฐ์!');
});
});
Mock์ Stub๊ณผ ๋น์ทํ์ง๋ง, ๊ธฐ๋๊ฐ(expectation)์ ๋ฏธ๋ฆฌ ์ค์ ํ๊ณ ๊ฒ์ฆํ ์ ์์ด.
describe('Sinon Mock', () => {
it('๊ฐ์ฒด์ ๋ฉ์๋ ํธ์ถ์ ๊ฒ์ฆํ๋ค', () => {
const user = {
save() {
return 'saved';
}
};
const mock = sinon.mock(user);
// ๊ธฐ๋๊ฐ ์ค์
mock.expects('save')
.once()
.withArgs({ name: '๊น์ฝ๋ฉ' })
.returns('success');
// ์ค์ ํธ์ถ
const result = user.save({ name: '๊น์ฝ๋ฉ' });
expect(result).to.equal('success');
// ๊ธฐ๋๊ฐ ๊ฒ์ฆ
mock.verify();
// ๋ณต์
mock.restore();
});
});
Spy: ํจ์ ํธ์ถ๋ง ์ถ์ ํ๊ณ ์ถ์ ๋. ์๋ ๋์์ ๊ทธ๋๋ก ์ ์ง.
โ "์ด ํจ์๊ฐ ํธ์ถ๋์๋์ง๋ง ํ์ธํ๊ณ ์ถ์ด"
Stub: ํจ์์ ๋์์ ์์ ํ ๋์ฒดํ๊ณ ์ถ์ ๋.
โ "API ํธ์ถ ๋์ ๊ฐ์ง ๋ฐ์ดํฐ๋ฅผ ๋ฐํํ๊ณ ์ถ์ด"
Mock: ๋ณต์กํ ๊ธฐ๋๊ฐ์ ์ค์ ํ๊ณ ๊ฒ์ฆํ๊ณ ์ถ์ ๋.
โ "์ด ๋ฉ์๋๊ฐ ์ ํํ ์ด๋ ๊ฒ ํธ์ถ๋์ด์ผ ํด"
๐ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง: ํ ์คํธ์ ์์ฑ๋ ์ธก์ ํ๊ธฐ
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๋ ํ ์คํธ๊ฐ ์ฝ๋์ ์ผ๋ง๋ ๋ง์ ๋ถ๋ถ์ ์คํํ๋์ง ๋ณด์ฌ์ฃผ๋ ์งํ์ผ. 100%๊ฐ ๋ชฉํ๋ ์๋์ง๋ง, ์ค์ํ ๋ถ๋ถ์ด ํ ์คํธ๋๊ณ ์๋์ง ํ์ธํ๋ ๋ฐ ์ ์ฉํด! ๐
Jest๋ ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ๋ด์ฅ๋์ด ์์ด์ ์ ๋ง ๊ฐ๋จํด:
# ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ
npm test -- --coverage
# ๋๋ package.json์ ์คํฌ๋ฆฝํธ ์ถ๊ฐ
"test:coverage": "jest --coverage"
์คํํ๋ฉด ์ด๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์:
--------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------------|---------|----------|---------|---------|
All files | 85.71 | 66.67 | 100 | 85.71 |
calculator.js | 85.71 | 66.67 | 100 | 85.71 |
--------------------------|---------|----------|---------|---------|
Statements (๊ตฌ๋ฌธ): ์คํ๋ ์ฝ๋ ๋ผ์ธ์ ๋น์จ
Branches (๋ถ๊ธฐ): if/else ๊ฐ์ ๋ถ๊ธฐ๋ฌธ์ ๋ชจ๋ ๊ฒฝ๋ก๊ฐ ์คํ๋ ๋น์จ
Functions (ํจ์): ํธ์ถ๋ ํจ์์ ๋น์จ
Lines (๋ผ์ธ): ์คํ๋ ์ฝ๋ ๋ผ์ธ์ ๋น์จ
์ปค๋ฒ๋ฆฌ์ง ์๊ณ๊ฐ์ ์ค์ ํ ์๋ ์์ด. jest.config.js์ ์ถ๊ฐ:
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Mocha๋ nyc(Istanbul)๋ฅผ ์ฌ์ฉํด์ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ์ธก์ ํด:
# nyc ์ค์น
npm install --save-dev nyc
# ์ปค๋ฒ๋ฆฌ์ง ์คํ
npx nyc mocha
# ๋๋ package.json์ ์คํฌ๋ฆฝํธ ์ถ๊ฐ
"test:coverage": "nyc mocha"
.nycrc ํ์ผ๋ก ์ค์ ์ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ด:
{
"all": true,
"include": ["src/**/*.js"],
"exclude": ["**/*.test.js", "**/*.spec.js"],
"reporter": ["html", "text", "lcov"],
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 80,
"statements": 80
}
์๋์ผ! ์ปค๋ฒ๋ฆฌ์ง๋ ์ฐธ๊ณ ์งํ์ผ ๋ฟ์ด์ผ. ์ค์ํ ๊ฑด ์๋ฏธ ์๋ ํ ์คํธ๋ฅผ ์์ฑํ๋ ๊ฑฐ์ง.
โข ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง: ๋์ ์ปค๋ฒ๋ฆฌ์ง ํ์ (90%+)
โข ์ ํธ๋ฆฌํฐ ํจ์: ์ค๊ฐ ์ ๋ (70-80%)
โข UI ์ปดํฌ๋ํธ: ์ํฉ์ ๋ฐ๋ผ ๋ค๋ฆ
โข ์ค์ ํ์ผ, ์์: ๋ฎ์๋ ๊ด์ฐฎ์
100%๋ฅผ ๋ชฉํ๋ก ํ๋ฉด ์คํ๋ ค ์๋ฏธ ์๋ ํ ์คํธ๋ฅผ ์์ฑํ๊ฒ ๋ ์ ์์ด. ๐
๐ฏ ์ค์ ํ ์คํธ ์์ฑ ํ๊ณผ ๋ฒ ์คํธ ํ๋ํฐ์ค
์ด์ ์ค์ ์์ ์ ์ฉํ ํ๋ค์ ์์๋ณด์! ์ด๊ฑด ๋ด๊ฐ ์ค์ ๋ก ํ๋ก์ ํธํ๋ฉด์ ๋ฐฐ์ด ๊ฒ๋ค์ด์ผ. ๐
ํ ์คํธ๋ Arrange(์ค๋น) - Act(์คํ) - Assert(๊ฒ์ฆ) ํจํด์ผ๋ก ์์ฑํ๋ฉด ์ฝ๊ธฐ ์ฌ์:
test('์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => {
// Arrange: ํ
์คํธ ์ค๋น
const userData = {
name: '๊น์ฝ๋ฉ',
email: 'test@example.com'
};
const userService = new UserService();
// Act: ์ค์ ๋์ ์คํ
const user = userService.createUser(userData);
// Assert: ๊ฒฐ๊ณผ ๊ฒ์ฆ
expect(user.name).toBe('๊น์ฝ๋ฉ');
expect(user.email).toBe('test@example.com');
expect(user.id).toBeDefined();
});
๊ฐ ํ ์คํธ๋ ๋ค๋ฅธ ํ ์คํธ์ ์ํฅ์ ์ฃผ๊ฑฐ๋ ๋ฐ์ผ๋ฉด ์ ๋ผ. ์์์ ์๊ด์์ด ์คํ๋์ด์ผ ํด!
// โ ๋์ ์: ํ
์คํธ๊ฐ ์๋ก ์์กด
let user;
test('์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => {
user = createUser({ name: '๊น์ฝ๋ฉ' });
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
test('์ฌ์ฉ์ ์ด๋ฆ์ ๋ณ๊ฒฝํ๋ค', () => {
user.name = '๋ฐ๊ฐ๋ฐ'; // ์ด์ ํ
์คํธ์ ์์กด!
expect(user.name).toBe('๋ฐ๊ฐ๋ฐ');
});
// โ
์ข์ ์: ๊ฐ ํ
์คํธ๊ฐ ๋
๋ฆฝ์
test('์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
test('์ฌ์ฉ์ ์ด๋ฆ์ ๋ณ๊ฒฝํ๋ค', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
user.name = '๋ฐ๊ฐ๋ฐ';
expect(user.name).toBe('๋ฐ๊ฐ๋ฐ');
});
ํ ์คํธ ์ด๋ฆ๋ง ๋ด๋ ๋ฌด์์ ํ ์คํธํ๋์ง ์ ์ ์์ด์ผ ํด:
// โ ๋์ ์
test('test1', () => { ... });
test('works', () => { ... });
// โ
์ข์ ์
test('์ ํจํ ์ด๋ฉ์ผ๋ก ์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => { ... });
test('์ค๋ณต๋ ์ด๋ฉ์ผ๋ก ์ฌ์ฉ์ ์์ฑ ์ ์๋ฌ๋ฅผ ๋์ง๋ค', () => { ... });
test('๋น ์ด๋ฆ์ผ๋ก ์ฌ์ฉ์ ์์ฑ ์ ValidationError๋ฅผ ๋์ง๋ค', () => { ... });
ํ ์คํธ๊ฐ ์คํจํ์ ๋ ์์ธ์ ์ฝ๊ฒ ์ฐพ์ ์ ์๋๋ก ํ๋์ ๊ฐ๋ ๋ง ํ ์คํธํด:
// โ ๋์ ์: ์ฌ๋ฌ ๊ฐ๋ฅผ ํ ๋ฒ์ ํ
์คํธ
test('์ฌ์ฉ์ ๊ธฐ๋ฅ', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
expect(user.name).toBe('๊น์ฝ๋ฉ');
user.updateEmail('new@example.com');
expect(user.email).toBe('new@example.com');
user.delete();
expect(user.isDeleted).toBe(true);
});
// โ
์ข์ ์: ๊ฐ๊ฐ ๋ถ๋ฆฌ
test('์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
expect(user.name).toBe('๊น์ฝ๋ฉ');
});
test('์ฌ์ฉ์ ์ด๋ฉ์ผ์ ์
๋ฐ์ดํธํ๋ค', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
user.updateEmail('new@example.com');
expect(user.email).toBe('new@example.com');
});
test('์ฌ์ฉ์๋ฅผ ์ญ์ ํ๋ค', () => {
const user = createUser({ name: '๊น์ฝ๋ฉ' });
user.delete();
expect(user.isDeleted).toBe(true);
});
์ ์์ ์ธ ๊ฒฝ์ฐ๋ฟ๋ง ์๋๋ผ ์์ธ ์ํฉ๋ ํ ์คํธํด์ผ ํด:
describe('๋๋์
ํจ์', () => {
test('์ ์์ ์ธ ๋๋์
', () => {
expect(divide(10, 2)).toBe(5);
});
test('0์ผ๋ก ๋๋๊ธฐ', () => {
expect(() => divide(10, 0)).toThrow();
});
test('์์ ๋๋์
', () => {
expect(divide(-10, 2)).toBe(-5);
});
test('์์์ ๋๋์
', () => {
expect(divide(10, 3)).toBeCloseTo(3.333, 2);
});
test('๋งค์ฐ ํฐ ์', () => {
expect(divide(Number.MAX_SAFE_INTEGER, 2)).toBeDefined();
});
});
๋ฐ๋ณต๋๋ ํ ์คํธ ๋ฐ์ดํฐ๋ ๋ณ๋๋ก ๊ด๋ฆฌํ๋ฉด ์ ์ง๋ณด์๊ฐ ์ฌ์:
// testData.js
const validUser = {
name: '๊น์ฝ๋ฉ',
email: 'valid@example.com',
age: 25
};
const invalidUsers = [
{ name: '', email: 'test@example.com', age: 25 }, // ๋น ์ด๋ฆ
{ name: '๊น์ฝ๋ฉ', email: 'invalid-email', age: 25 }, // ์๋ชป๋ ์ด๋ฉ์ผ
{ name: '๊น์ฝ๋ฉ', email: 'test@example.com', age: -1 }, // ์์ ๋์ด
];
module.exports = { validUser, invalidUsers };
// user.test.js
const { validUser, invalidUsers } = require('./testData');
test('์ ํจํ ์ฌ์ฉ์๋ฅผ ์์ฑํ๋ค', () => {
const user = createUser(validUser);
expect(user).toBeDefined();
});
invalidUsers.forEach((invalidUser, index) => {
test(`์๋ชป๋ ์ฌ์ฉ์ ๋ฐ์ดํฐ ${index + 1}`, () => {
expect(() => createUser(invalidUser)).toThrow();
});
});
- โ ํ ์คํธ ์ด๋ฆ์ด ๋ช ํํ๊ฐ?
- โ ๊ฐ ํ ์คํธ๊ฐ ๋ ๋ฆฝ์ ์ธ๊ฐ?
- โ AAA ํจํด์ ๋ฐ๋ฅด๋๊ฐ?
- โ ์ ์ ์ผ์ด์ค์ ์์ธ ์ผ์ด์ค๋ฅผ ๋ชจ๋ ๋ค๋ฃจ๋๊ฐ?
- โ ํ ์คํธ๊ฐ ๋น ๋ฅด๊ฒ ์คํ๋๋๊ฐ?
- โ ํ ์คํธ๊ฐ ์คํจํ์ ๋ ์์ธ์ ์ฝ๊ฒ ์ฐพ์ ์ ์๋๊ฐ?
- โ ์ธ๋ถ ์์กด์ฑ์ ์ ์ ํ ๋ชจํนํ๋๊ฐ?
๐ ์ค์ ํ๋ก์ ํธ: TODO ์ฑ ํ ์คํธํ๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ์์ ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ TODO ์ฑ์ ํ ์คํธ๋ฅผ ์์ฑํด๋ณผ๊ฒ. ๐ฏ
// todoService.js
class TodoService {
constructor() {
this.todos = [];
this.nextId = 1;
}
addTodo(title) {
if (!title || title.trim() === '') {
throw new Error('์ ๋ชฉ์ ํ์์
๋๋ค');
}
const todo = {
id: this.nextId++,
title: title.trim(),
completed: false,
createdAt: new Date()
};
this.todos.push(todo);
return todo;
}
getTodos() {
return [...this.todos];
}
getTodoById(id) {
const todo = this.todos.find(t => t.id === id);
if (!todo) {
throw new Error('TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
}
return todo;
}
updateTodo(id, updates) {
const todo = this.getTodoById(id);
if (updates.title !== undefined) {
if (!updates.title || updates.title.trim() === '') {
throw new Error('์ ๋ชฉ์ ํ์์
๋๋ค');
}
todo.title = updates.title.trim();
}
if (updates.completed !== undefined) {
todo.completed = updates.completed;
}
return todo;
}
deleteTodo(id) {
const index = this.todos.findIndex(t => t.id === id);
if (index === -1) {
throw new Error('TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
}
this.todos.splice(index, 1);
return true;
}
getCompletedTodos() {
return this.todos.filter(t => t.completed);
}
getActiveTodos() {
return this.todos.filter(t => !t.completed);
}
clearCompleted() {
const completedCount = this.getCompletedTodos().length;
this.todos = this.getActiveTodos();
return completedCount;
}
}
module.exports = TodoService;
// todoService.test.js
const TodoService = require('./todoService');
describe('TodoService', () => {
let todoService;
beforeEach(() => {
todoService = new TodoService();
});
describe('addTodo()', () => {
test('์๋ก์ด TODO๋ฅผ ์ถ๊ฐํ๋ค', () => {
const todo = todoService.addTodo('ํ
์คํธ ์์ฑํ๊ธฐ');
expect(todo).toMatchObject({
id: 1,
title: 'ํ
์คํธ ์์ฑํ๊ธฐ',
completed: false
});
expect(todo.createdAt).toBeInstanceOf(Date);
});
test('๊ณต๋ฐฑ์ ์ ๊ฑฐํ๋ค', () => {
const todo = todoService.addTodo(' ํ
์คํธ ์์ฑํ๊ธฐ ');
expect(todo.title).toBe('ํ
์คํธ ์์ฑํ๊ธฐ');
});
test('๋น ์ ๋ชฉ์ผ๋ก ์ถ๊ฐ ์ ์๋ฌ๋ฅผ ๋์ง๋ค', () => {
expect(() => todoService.addTodo('')).toThrow('์ ๋ชฉ์ ํ์์
๋๋ค');
expect(() => todoService.addTodo(' ')).toThrow('์ ๋ชฉ์ ํ์์
๋๋ค');
});
test('์ฌ๋ฌ TODO๋ฅผ ์ถ๊ฐํ๋ฉด ID๊ฐ ์ฆ๊ฐํ๋ค', () => {
const todo1 = todoService.addTodo('์ฒซ ๋ฒ์งธ');
const todo2 = todoService.addTodo('๋ ๋ฒ์งธ');
expect(todo1.id).toBe(1);
expect(todo2.id).toBe(2);
});
});
describe('getTodos()', () => {
test('๋ชจ๋ TODO๋ฅผ ๋ฐํํ๋ค', () => {
todoService.addTodo('์ฒซ ๋ฒ์งธ');
todoService.addTodo('๋ ๋ฒ์งธ');
const todos = todoService.getTodos();
expect(todos).toHaveLength(2);
expect(todos[0].title).toBe('์ฒซ ๋ฒ์งธ');
expect(todos[1].title).toBe('๋ ๋ฒ์งธ');
});
test('๋น ๋ฐฐ์ด์ ๋ฐํํ๋ค', () => {
const todos = todoService.getTodos();
expect(todos).toEqual([]);
});
test('์๋ณธ ๋ฐฐ์ด์ ์์ ํ์ง ์๋๋ค', () => {
todoService.addTodo('ํ
์คํธ');
const todos = todoService.getTodos();
todos.push({ id: 999, title: 'ํดํน' });
expect(todoService.getTodos()).toHaveLength(1);
});
});
describe('getTodoById()', () => {
test('ID๋ก TODO๋ฅผ ์ฐพ๋๋ค', () => {
const added = todoService.addTodo('ํ
์คํธ');
const found = todoService.getTodoById(added.id);
expect(found).toEqual(added);
});
test('์กด์ฌํ์ง ์๋ ID๋ ์๋ฌ๋ฅผ ๋์ง๋ค', () => {
expect(() => todoService.getTodoById(999)).toThrow('TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
});
});
describe('updateTodo()', () => {
test('์ ๋ชฉ์ ์
๋ฐ์ดํธํ๋ค', () => {
const todo = todoService.addTodo('์๋ ์ ๋ชฉ');
todoService.updateTodo(todo.id, { title: '์๋ก์ด ์ ๋ชฉ' });
expect(todo.title).toBe('์๋ก์ด ์ ๋ชฉ');
});
test('์๋ฃ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ค', () => {
const todo = todoService.addTodo('ํ
์คํธ');
todoService.updateTodo(todo.id, { completed: true });
expect(todo.completed).toBe(true);
});
test('์ฌ๋ฌ ํ๋๋ฅผ ๋์์ ์
๋ฐ์ดํธํ๋ค', () => {
const todo = todoService.addTodo('ํ
์คํธ');
todoService.updateTodo(todo.id, {
title: '์ ์ ๋ชฉ',
completed: true
});
expect(todo.title).toBe('์ ์ ๋ชฉ');
expect(todo.completed).toBe(true);
});
test('๋น ์ ๋ชฉ์ผ๋ก ์
๋ฐ์ดํธ ์ ์๋ฌ๋ฅผ ๋์ง๋ค', () => {
const todo = todoService.addTodo('ํ
์คํธ');
expect(() => {
todoService.updateTodo(todo.id, { title: '' });
}).toThrow('์ ๋ชฉ์ ํ์์
๋๋ค');
});
});
describe('deleteTodo()', () => {
test('TODO๋ฅผ ์ญ์ ํ๋ค', () => {
const todo = todoService.addTodo('์ญ์ ํ TODO');
todoService.deleteTodo(todo.id);
expect(todoService.getTodos()).toHaveLength(0);
});
test('์กด์ฌํ์ง ์๋ TODO ์ญ์ ์ ์๋ฌ๋ฅผ ๋์ง๋ค', () => {
expect(() => todoService.deleteTodo(999)).toThrow('TODO๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค');
});
});
describe('ํํฐ๋ง ๊ธฐ๋ฅ', () => {
beforeEach(() => {
todoService.addTodo('ํ ์ผ 1');
todoService.addTodo('ํ ์ผ 2');
todoService.addTodo('ํ ์ผ 3');
todoService.updateTodo(1, { completed: true });
todoService.updateTodo(3, { completed: true });
});
test('์๋ฃ๋ TODO๋ง ๋ฐํํ๋ค', () => {
const completed = todoService.getCompletedTodos();
expect(completed).toHaveLength(2);
expect(completed.every(t => t.completed)).toBe(true);
});
test('๋ฏธ์๋ฃ TODO๋ง ๋ฐํํ๋ค', () => {
const active = todoService.getActiveTodos();
expect(active).toHaveLength(1);
expect(active.every(t => !t.completed)).toBe(true);
});
test('์๋ฃ๋ TODO๋ฅผ ๋ชจ๋ ์ญ์ ํ๋ค', () => {
const deletedCount = todoService.clearCompleted();
expect(deletedCount).toBe(2);
expect(todoService.getTodos()).toHaveLength(1);
expect(todoService.getTodos()[0].completed).toBe(false);
});
});
});
ํ ์คํธ๋ฅผ ์คํํด๋ณด๋ฉด:
npm test
PASS ./todoService.test.js
TodoService
addTodo()
โ ์๋ก์ด TODO๋ฅผ ์ถ๊ฐํ๋ค (4ms)
โ ๊ณต๋ฐฑ์ ์ ๊ฑฐํ๋ค (1ms)
โ ๋น ์ ๋ชฉ์ผ๋ก ์ถ๊ฐ ์ ์๋ฌ๋ฅผ ๋์ง๋ค (2ms)
โ ์ฌ๋ฌ TODO๋ฅผ ์ถ๊ฐํ๋ฉด ID๊ฐ ์ฆ๊ฐํ๋ค (1ms)
getTodos()
โ ๋ชจ๋ TODO๋ฅผ ๋ฐํํ๋ค
โ ๋น ๋ฐฐ์ด์ ๋ฐํํ๋ค
โ ์๋ณธ ๋ฐฐ์ด์ ์์ ํ์ง ์๋๋ค (1ms)
getTodoById()
โ ID๋ก TODO๋ฅผ ์ฐพ๋๋ค
โ ์กด์ฌํ์ง ์๋ ID๋ ์๋ฌ๋ฅผ ๋์ง๋ค (1ms)
updateTodo()
โ ์ ๋ชฉ์ ์
๋ฐ์ดํธํ๋ค
โ ์๋ฃ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ค
โ ์ฌ๋ฌ ํ๋๋ฅผ ๋์์ ์
๋ฐ์ดํธํ๋ค (1ms)
โ ๋น ์ ๋ชฉ์ผ๋ก ์
๋ฐ์ดํธ ์ ์๋ฌ๋ฅผ ๋์ง๋ค
deleteTodo()
โ TODO๋ฅผ ์ญ์ ํ๋ค
โ ์กด์ฌํ์ง ์๋ TODO ์ญ์ ์ ์๋ฌ๋ฅผ ๋์ง๋ค (1ms)
ํํฐ๋ง ๊ธฐ๋ฅ
โ ์๋ฃ๋ TODO๋ง ๋ฐํํ๋ค
โ ๋ฏธ์๋ฃ TODO๋ง ๋ฐํํ๋ค
โ ์๋ฃ๋ TODO๋ฅผ ๋ชจ๋ ์ญ์ ํ๋ค (1ms)
Test Suites: 1 passed, 1 total
Tests: 18 passed, 18 total
Time: 1.234s
์๋ฒฝํด! ๐ ๋ชจ๋ ํ ์คํธ๊ฐ ํต๊ณผํ์ด. ์ด์ ์ด ์ฝ๋๋ฅผ ์์ ์๊ฒ ๋ฆฌํฉํ ๋งํ๊ฑฐ๋ ์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์์ด.
๐ TDD(Test-Driven Development): ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ
TDD๋ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํ๊ณ , ๊ทธ ํ ์คํธ๋ฅผ ํต๊ณผํ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฐ๋ฐ ๋ฐฉ๋ฒ๋ก ์ด์ผ. ์ฒ์์ ์ด์ํ๊ฒ ๋๊ปด์ง ์ ์์ง๋ง, ์ต์ํด์ง๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ๋๊ตฌ๊ฐ ๋ผ! ๐ด๐ขโป๏ธ
๐ด Red (์คํจํ๋ ํ ์คํธ ์์ฑ)
์์ง ๊ตฌํ๋์ง ์์ ๊ธฐ๋ฅ์ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํด. ๋น์ฐํ ์คํจํ๊ฒ ์ง?
๐ข Green (ํ ์คํธ๋ฅผ ํต๊ณผํ๋ ์ต์ํ์ ์ฝ๋ ์์ฑ)
ํ ์คํธ๋ฅผ ํต๊ณผํ ์ ์๋ ๊ฐ์ฅ ๊ฐ๋จํ ์ฝ๋๋ฅผ ์์ฑํด. ์๋ฒฝํ ํ์ ์์ด!
โป๏ธ Refactor (์ฝ๋ ๊ฐ์ )
ํ ์คํธ๊ฐ ํต๊ณผํ๋ ์ํ๋ฅผ ์ ์งํ๋ฉด์ ์ฝ๋๋ฅผ ๊ฐ์ ํด.
์ค์ ์์๋ฅผ ๋ณด์. ๋ฌธ์์ด์ ๋ฐ๋๋ก ๋ค์ง๋ ํจ์๋ฅผ TDD๋ก ๋ง๋ค์ด๋ณผ๊ฒ:
// reverse.test.js
const reverse = require('./reverse');
describe('reverse()', () => {
test('๋ฌธ์์ด์ ๋ค์ง๋๋ค', () => {
expect(reverse('hello')).toBe('olleh');
});
});
์ด ์ํ์์ ํ ์คํธ๋ฅผ ์คํํ๋ฉด? ๋น์ฐํ ์คํจํด! reverse ํจ์๊ฐ ์์ง ์์ผ๋๊น. ๐ด
// reverse.js
function reverse(str) {
return str.split('').reverse().join('');
}
module.exports = reverse;
์ด์ ํ ์คํธ๋ฅผ ์คํํ๋ฉด ํต๊ณผํด! ๐ข
describe('reverse()', () => {
test('๋ฌธ์์ด์ ๋ค์ง๋๋ค', () => {
expect(reverse('hello')).toBe('olleh');
});
test('๋น ๋ฌธ์์ด์ ์ฒ๋ฆฌํ๋ค', () => {
expect(reverse('')).toBe('');
});
test('ํ ๊ธ์๋ฅผ ์ฒ๋ฆฌํ๋ค', () => {
expect(reverse('a')).toBe('a');
});
test('๊ณต๋ฐฑ์ ํฌํจํ ๋ฌธ์์ด์ ์ฒ๋ฆฌํ๋ค', () => {
expect(reverse('hello world')).toBe('dlrow olleh');
});
test('ํน์๋ฌธ์๋ฅผ ์ฒ๋ฆฌํ๋ค', () => {
expect(reverse('hello!')).toBe('!olleh');
});
});
๋ชจ๋ ํ ์คํธ๊ฐ ํต๊ณผํ๋ค! ๐
ํ์ฌ ์ฝ๋๋ ์ด๋ฏธ ๊ฐ๋จํ์ง๋ง, ๋ง์ฝ ๋ ๋ณต์กํ๋ค๋ฉด ์ด ๋จ๊ณ์์ ๊ฐ์ ํ ์ ์์ด. ์ค์ํ ๊ฑด ํ ์คํธ๊ฐ ๊ณ์ ํต๊ณผํ๋ ์ํ๋ฅผ ์ ์งํ๋ ๊ฑฐ์ผ!
โข ๋ช ํํ ์๊ตฌ์ฌํญ: ํ ์คํธ๊ฐ ๊ณง ๋ช ์ธ์๊ฐ ๋ผ
โข ๋ฒ๊ทธ ๊ฐ์: ์ฝ๋๋ฅผ ์์ฑํ๊ธฐ ์ ์ ์์ธ ์ผ์ด์ค๋ฅผ ์๊ฐํ๊ฒ ๋ผ
โข ๋ฆฌํฉํ ๋ง ์์ ๊ฐ: ํ ์คํธ๊ฐ ์์ผ๋ ์์ ํ๊ฒ ์ฝ๋๋ฅผ ๊ฐ์ ํ ์ ์์ด
โข ๋ฌธ์ํ: ํ ์คํธ ์ฝ๋๊ฐ ์ฌ์ฉ ์์๊ฐ ๋ผ
โข ์ค๊ณ ๊ฐ์ : ํ ์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋๋ ๋ณดํต ์ข์ ์ค๊ณ์ผ
์ฒ์์ ์ด์ํ๊ณ ๋๋ฆด ์ ์์ง๋ง, ์ต์ํด์ง๋ฉด ์คํ๋ ค ๊ฐ๋ฐ ์๋๊ฐ ๋นจ๋ผ์ ธ! ๐
๐ ์ฌ๋ฅ๋ท์์ ํ ์คํ ์คํฌ ํ์ฉํ๊ธฐ
ํ ์คํ ๋ฅ๋ ฅ์ ํ๋ก๊ทธ๋๋จธ๋ก์ ์ ๋ง ์ค์ํ ์คํฌ์ด์ผ. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ํ๋ก๊ทธ๋๋ฐ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ๋๋ ํฐ ์ฐจ๋ณํ ํฌ์ธํธ๊ฐ ๋ ์ ์์ด! ๐ผ
1. ํฌํธํด๋ฆฌ์ค์ ํ ์คํธ ์ฝ๋ ํฌํจ
GitHub ์ ์ฅ์์ ํ ์คํธ ์ฝ๋๋ฅผ ํจ๊ป ์ฌ๋ ค. ์ปค๋ฒ๋ฆฌ์ง ๋ฐฐ์ง๋ ์ถ๊ฐํ๋ฉด ๋ ์ข์!
2. ํ ์คํ ์ ์๋น์ค ํญ๋ชฉ์ ์ถ๊ฐ
"๋จ์ ํ ์คํธ ์์ฑ ํฌํจ", "Jest/Mocha ํ ์คํธ ์ฝ๋ ์ ๊ณต" ๊ฐ์ ์ต์ ์ ์ถ๊ฐํด๋ด.
3. ํ์ง ๋ณด์ฆ ๊ฐ์กฐ
"ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง 80% ์ด์ ๋ณด์ฅ", "TDD ๋ฐฉ์์ผ๋ก ๊ฐ๋ฐ" ๊ฐ์ ๋ฌธ๊ตฌ๋ก ์ ๋ขฐ๋๋ฅผ ๋์ฌ.
4. ํ ์คํ ์ปจ์คํ ์๋น์ค
๊ธฐ์กด ํ๋ก์ ํธ์ ํ ์คํธ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋, ํ ์คํ ํ๊ฒฝ์ ๊ตฌ์ถํด์ฃผ๋ ์๋น์ค๋ ์ ๊ณตํ ์ ์์ด.
5. ๊ต์ก ์ฝํ ์ธ ์ ์
ํ ์คํ ํํ ๋ฆฌ์ผ์ด๋ ๊ฐ์ ์๋ฃ๋ฅผ ๋ง๋ค์ด์ ํ๋งคํ ์๋ ์์ง!
์ค์ ๋ก ํด๋ผ์ด์ธํธ ์
์ฅ์์ ์๊ฐํด๋ด. ๋ ๊ฐ๋ฐ์๊ฐ ์๋๋ฐ:
๊ฐ๋ฐ์ A: "์ฝ๋ ์์ฑํด๋๋ฆฝ๋๋ค"
๊ฐ๋ฐ์ B: "ํ
์คํธ ์ฝ๋๊ฐ ํฌํจ๋ ๊ณ ํ์ง ์ฝ๋๋ฅผ ์์ฑํด๋๋ฆฝ๋๋ค. ์ปค๋ฒ๋ฆฌ์ง 80% ์ด์ ๋ณด์ฅ"
๋๊ตฌํํ
๋งก๊ธฐ๊ณ ์ถ์๊น? ๋น์ฐํ B์ง! ๐
ํ
์คํ
๋ฅ๋ ฅ์ ๋จ์ํ ๋ฒ๊ทธ๋ฅผ ์ค์ด๋ ๊ฒ ์ด์์ ๊ฐ์น๊ฐ ์์ด. ์ ๋ฌธ์ฑ๊ณผ ์ ๋ขฐ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ ๊ฐ๋ ฅํ ์งํ๊ฐ ๋๊ฑฐ๋ .
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์, ์ ๋ง ๊ธด ์ฌ์ ์ด์์ง? ๐
์ฐ๋ฆฌ๊ฐ ํจ๊ป ๋ฐฐ์ด ๋ด์ฉ์ ์ ๋ฆฌํด๋ณผ๊น:
โ
ํ
์คํธ ์ฝ๋์ ์ค์์ฑ๊ณผ ํ์์ฑ
โ
๋จ์ ํ
์คํธ์ ๊ฐ๋
๊ณผ ํน์ง
โ
Jest์ ์ค์น, ์ค์ , ์ฌ์ฉ๋ฒ
โ
Mocha + Chai + Sinon ์กฐํฉ
โ
๋น๋๊ธฐ ์ฝ๋ ํ
์คํธ ๋ฐฉ๋ฒ
โ
Mock, Stub, Spy ํ์ฉ
โ
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ์ธก์
โ
ํ
์คํธ ์์ฑ ๋ฒ ์คํธ ํ๋ํฐ์ค
โ
TDD ๊ฐ๋ฐ ๋ฐฉ๋ฒ๋ก
โ
์ค์ ํ๋ก์ ํธ ์์
ํ
์คํ
์ ์ฒ์์ ๊ท์ฐฎ๊ณ ์๊ฐ ๋ญ๋น์ฒ๋ผ ๋๊ปด์ง ์ ์์ด. ๋๋ ๊ทธ๋ฌ๊ฑฐ๋ . ํ์ง๋ง ํ๋ก์ ํธ๊ฐ ์ปค์ง๊ณ , ํ์
์ ํ๊ฒ ๋๋ฉด์ ํ
์คํธ ์ฝ๋์ ์ง๊ฐ๋ฅผ ๋๋ผ๊ฒ ๋์ด. ๐
ํ
์คํธ ์ฝ๋๋ ๋ฏธ๋์ ๋๋ฅผ ์ํ ์ ๋ฌผ์ด์ผ. 6๊ฐ์ ํ์ ๋ด๊ฐ ์์ฑํ ์ฝ๋๋ฅผ ๋ค์ ๋ดค์ ๋, ํ
์คํธ๊ฐ ์์ผ๋ฉด ์์ ์๊ฒ ์์ ํ ์ ์๊ฑฐ๋ .
์ฒ์๋ถํฐ ์๋ฒฝํ๊ฒ ํ๋ ค๊ณ ํ์ง ๋ง. ์์ ํจ์ ํ๋๋ถํฐ ํ
์คํธ๋ฅผ ์์ฑํด๋ณด๊ณ , ์ ์ ๋ฒ์๋ฅผ ๋ํ๊ฐ๋ฉด ๋ผ. ๊ทธ๋ฌ๋ค ๋ณด๋ฉด ์ด๋์ ํ
์คํธ ์์ฑ์ด ์์ฐ์ค๋ฌ์์ง ๊ฑฐ์ผ! ๐ช
๊ทธ๋ฆฌ๊ณ ๊ธฐ์ตํด. ์ข์ ํ
์คํธ๋ ์ข์ ์ฝ๋๋ฅผ ๋ง๋ค์ด. ํ
์คํธํ๊ธฐ ์ฌ์ด ์ฝ๋๋ ๋ณดํต ์ ์ค๊ณ๋ ์ฝ๋๊ฑฐ๋ . ํ
์คํ
์ ๋ฐฐ์ฐ๋ฉด์ ์์ฐ์ค๋ฝ๊ฒ ๋ ๋์ ๊ฐ๋ฐ์๊ฐ ๋๋ ๊ฑฐ์ง! ๐
์, ์ด์ ๋น์ ์ฐจ๋ก์ผ! ์ค๋ ๋ฐฐ์ด ๋ด์ฉ์ ๋ฐํ์ผ๋ก ์ค์ ํ๋ก์ ํธ์ ํ
์คํธ๋ฅผ ์ถ๊ฐํด๋ด. ์ฒ์์ ์ด๋ ต๊ฒ ์ง๋ง, ํฌ๊ธฐํ์ง ๋ง๊ณ ๊ณ์ ์ฐ์ตํ๋ฉด ๋ถ๋ช
์ค๋ ฅ์ด ๋ ๊ฑฐ์ผ.
ํ์ดํ
! ๐ ๊ทธ๋ฆฌ๊ณ ๊ถ๊ธํ ๊ฒ ์์ผ๋ฉด ์ธ์ ๋ ๋ฌผ์ด๋ด. ์ฐ๋ฆฌ ๋ชจ๋ ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์๊ฐ ๋์! ๐
๊ด๋ จ ํค์๋
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.