์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿงช JavaScript ํ…Œ์ŠคํŒ… ์™„๋ฒฝ ๊ฐ€์ด๋“œ: Jest์™€ Mocha๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ๋ชจ๋“  ๊ฒƒ

๐Ÿงช JavaScript ํ…Œ์ŠคํŒ… ์™„๋ฒฝ ๊ฐ€์ด๋“œ: Jest์™€ Mocha๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ๋ชจ๋“  ๊ฒƒ

์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ๋ฐฐ์šฐ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ๋ฒ• ๐Ÿ’ก

Jest ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ Mocha ์œ ์—ฐํ•œ ํ…Œ์ŠคํŒ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ Unit Testing ์ฝ”๋“œ ํ’ˆ์งˆ ํ–ฅ์ƒ ๋ฒ„๊ทธ ๊ฐ์†Œ ์•ˆ์ •์„ฑ ๋ฆฌํŒฉํ† ๋ง ์ž์‹ ๊ฐ JavaScript ํ…Œ์ŠคํŒ… ์ƒํƒœ๊ณ„

๐ŸŽฏ ์™œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ 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๋Š” ์ •๋ง ์‹œ์ž‘ํ•˜๊ธฐ ์‰ฌ์›Œ. ๐Ÿ˜Ž

๐Ÿ“ฆ 1๋‹จ๊ณ„: Jest ์„ค์น˜ํ•˜๊ธฐ

ํ”„๋กœ์ ํŠธ ํด๋”์—์„œ ํ„ฐ๋ฏธ๋„์„ ์—ด๊ณ  ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด:

# npm์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
npm install --save-dev jest

# yarn์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
yarn add --dev jest

--save-dev ์˜ต์…˜์€ ๊ฐœ๋ฐœ ์˜์กด์„ฑ์œผ๋กœ ์„ค์น˜ํ•œ๋‹ค๋Š” ๋œป์ด์•ผ. ํ…Œ์ŠคํŠธ ๋„๊ตฌ๋Š” ์‹ค์ œ ๋ฐฐํฌํ•  ๋•Œ๋Š” ํ•„์š” ์—†์œผ๋‹ˆ๊นŒ!

โš™๏ธ 2๋‹จ๊ณ„: package.json ์„ค์ •ํ•˜๊ธฐ

package.json ํŒŒ์ผ์„ ์—ด์–ด์„œ scripts ๋ถ€๋ถ„์— ํ…Œ์ŠคํŠธ ๋ช…๋ น์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

์ด์ œ npm test ๋ช…๋ น์–ด๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰

โœ๏ธ 3๋‹จ๊ณ„: ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์ž‘์„ฑํ•˜๊ธฐ

๊ฐ„๋‹จํ•œ ํ•จ์ˆ˜์™€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž. ๋จผ์ € ํ…Œ์ŠคํŠธํ•  ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด:

// 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(): ํ•จ์ˆ˜๊ฐ€ ์—๋Ÿฌ๋ฅผ ๋˜์ง€๋Š”์ง€ ํ™•์ธํ•ด.
โ–ถ๏ธ 4๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ์‹คํ–‰ํ•˜๊ธฐ

ํ„ฐ๋ฏธ๋„์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด๋ฅผ ์ž…๋ ฅํ•ด:

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}$/);
  });
  
});
๐Ÿ’ก not์„ ์‚ฌ์šฉํ•œ ๋ฐ˜๋Œ€ ํ…Œ์ŠคํŠธ:
๋ชจ๋“  matcher ์•ž์— .not์„ ๋ถ™์ด๋ฉด ๋ฐ˜๋Œ€๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด!

expect(user.age).not.toBe(30);
expect(user.hobbies).not.toContain('๊ฒŒ์ž„');

โฑ๏ธ ๋น„๋™๊ธฐ ์ฝ”๋“œ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์‹ค์ œ ๊ฐœ๋ฐœ์—์„œ๋Š” API ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๊ฐ™์€ ๋น„๋™๊ธฐ ์ž‘์—…์ด ๋งŽ์•„. Jest์—์„œ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐ŸŒ

๐Ÿ”„ Promise ํ…Œ์ŠคํŠธํ•˜๊ธฐ
// 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์„ ์‚ฌ์šฉํ•˜๋ฉด ์‹ค์ œ ์˜์กด์„ฑ ์—†์ด๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽช

๐Ÿƒ 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 ์„ค์น˜ ๋ฐ ์„ค์ •

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"
  }
}
โœ๏ธ 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');
    });
  });
  
});
๐Ÿ” Mocha์˜ ๊ตฌ์กฐ

describe(): ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ(๊ทธ๋ฃน)๋ฅผ ์ •์˜. ์ค‘์ฒฉ ๊ฐ€๋Šฅ!

it(): ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค. test()์™€ ๊ฐ™์€ ์—ญํ• 

beforeEach(): ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ์‹คํ–‰
afterEach(): ๊ฐ ํ…Œ์ŠคํŠธ ํ›„์— ์‹คํ–‰
before(): ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ „์— ํ•œ ๋ฒˆ ์‹คํ–‰
after(): ๋ชจ๋“  ํ…Œ์ŠคํŠธ ํ›„์— ํ•œ ๋ฒˆ ์‹คํ–‰
๐ŸŽจ Chai์˜ ๋‹ค์–‘ํ•œ Assertion ์Šคํƒ€์ผ

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๋„ ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ๋ฅผ ํ›Œ๋ฅญํ•˜๊ฒŒ ์ง€์›ํ•ด. ์—ฌ๋Ÿฌ ๋ฐฉ๋ฒ•์ด ์žˆ๋Š”๋ฐ ํ•˜๋‚˜์”ฉ ์•Œ์•„๋ณด์ž! โฑ๏ธ

๐ŸŽฏ Callback ๋ฐฉ์‹
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(); // ํ…Œ์ŠคํŠธ ์™„๋ฃŒ๋ฅผ ์•Œ๋ฆผ
    });
  });
  
});
๐ŸŽฏ Promise ๋ฐฉ์‹
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('๊น€์ฝ”๋”ฉ');
    });
  });
  
});
๐ŸŽฏ Async/Await ๋ฐฉ์‹ (์ถ”์ฒœ!)
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 ํƒ€์ž„์•„์›ƒ ์„ค์ •:
๊ธฐ๋ณธ์ ์œผ๋กœ Mocha๋Š” 2์ดˆ ์•ˆ์— ํ…Œ์ŠคํŠธ๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์œผ๋ฉด ์‹คํŒจ๋กœ ์ฒ˜๋ฆฌํ•ด. ๊ธด ์ž‘์—…์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ํƒ€์ž„์•„์›ƒ์„ ๋Š˜๋ ค์ค˜์•ผ ํ•ด:

it('๊ธด ์ž‘์—…', async function() {
this.timeout(5000); // 5์ดˆ๋กœ ์„ค์ •
// ํ…Œ์ŠคํŠธ ์ฝ”๋“œ...
});

๐ŸŽญ Mocha์—์„œ Mocking: Sinon ํ™œ์šฉํ•˜๊ธฐ

Mocha๋Š” ๋ชจํ‚น ๊ธฐ๋Šฅ์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์ง€ ์•Š์•„์„œ Sinon์ด๋ผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ฃผ๋กœ ์‚ฌ์šฉํ•ด. Sinon์€ ๋งค์šฐ ๊ฐ•๋ ฅํ•œ ๋ชจํ‚น ๋„๊ตฌ์•ผ! ๐Ÿ’ช

๐Ÿ“ฆ Sinon ์„ค์น˜
npm install --save-dev sinon
๐ŸŽช Spy ์‚ฌ์šฉํ•˜๊ธฐ

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 ์‚ฌ์šฉํ•˜๊ธฐ

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 ์‚ฌ์šฉํ•˜๊ธฐ

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 vs Stub vs Mock, ๋ญ˜ ์จ์•ผ ํ• ๊นŒ?

Spy: ํ•จ์ˆ˜ ํ˜ธ์ถœ๋งŒ ์ถ”์ ํ•˜๊ณ  ์‹ถ์„ ๋•Œ. ์›๋ž˜ ๋™์ž‘์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€.
โ†’ "์ด ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€๋งŒ ํ™•์ธํ•˜๊ณ  ์‹ถ์–ด"

Stub: ํ•จ์ˆ˜์˜ ๋™์ž‘์„ ์™„์ „ํžˆ ๋Œ€์ฒดํ•˜๊ณ  ์‹ถ์„ ๋•Œ.
โ†’ "API ํ˜ธ์ถœ ๋Œ€์‹  ๊ฐ€์งœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์‹ถ์–ด"

Mock: ๋ณต์žกํ•œ ๊ธฐ๋Œ€๊ฐ’์„ ์„ค์ •ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๊ณ  ์‹ถ์„ ๋•Œ.
โ†’ "์ด ๋ฉ”์„œ๋“œ๊ฐ€ ์ •ํ™•ํžˆ ์ด๋ ‡๊ฒŒ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•ด"

๐Ÿ“Š ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€: ํ…Œ์ŠคํŠธ์˜ ์™„์„ฑ๋„ ์ธก์ •ํ•˜๊ธฐ

์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ์ฝ”๋“œ์˜ ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๋ถ€๋ถ„์„ ์‹คํ–‰ํ–ˆ๋Š”์ง€ ๋ณด์—ฌ์ฃผ๋Š” ์ง€ํ‘œ์•ผ. 100%๊ฐ€ ๋ชฉํ‘œ๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด ํ…Œ์ŠคํŠธ๋˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ด! ๐Ÿ“ˆ

๐Ÿƒ Jest์—์„œ ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€

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 ์‚ฌ์šฉ)

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
}
๐Ÿ’ก ์ปค๋ฒ„๋ฆฌ์ง€ 100%๊ฐ€ ๋ชฉํ‘œ์ผ๊นŒ?

์•„๋‹ˆ์•ผ! ์ปค๋ฒ„๋ฆฌ์ง€๋Š” ์ฐธ๊ณ  ์ง€ํ‘œ์ผ ๋ฟ์ด์•ผ. ์ค‘์š”ํ•œ ๊ฑด ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฑฐ์ง€.

โ€ข ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง: ๋†’์€ ์ปค๋ฒ„๋ฆฌ์ง€ ํ•„์š” (90%+)
โ€ข ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜: ์ค‘๊ฐ„ ์ •๋„ (70-80%)
โ€ข UI ์ปดํฌ๋„ŒํŠธ: ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ค๋ฆ„
โ€ข ์„ค์ • ํŒŒ์ผ, ์ƒ์ˆ˜: ๋‚ฎ์•„๋„ ๊ดœ์ฐฎ์Œ

100%๋ฅผ ๋ชฉํ‘œ๋กœ ํ•˜๋ฉด ์˜คํžˆ๋ ค ์˜๋ฏธ ์—†๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ๋  ์ˆ˜ ์žˆ์–ด. ๐Ÿ˜…

๐ŸŽฏ ์‹ค์ „ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํŒ๊ณผ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

์ด์ œ ์‹ค์ „์—์„œ ์œ ์šฉํ•œ ํŒ๋“ค์„ ์•Œ์•„๋ณด์ž! ์ด๊ฑด ๋‚ด๊ฐ€ ์‹ค์ œ๋กœ ํ”„๋กœ์ ํŠธํ•˜๋ฉด์„œ ๋ฐฐ์šด ๊ฒƒ๋“ค์ด์•ผ. ๐Ÿ’Ž

โœ… 1. AAA ํŒจํ„ด ๋”ฐ๋ฅด๊ธฐ

ํ…Œ์ŠคํŠธ๋Š” 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();
});
โœ… 2. ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•ด

๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜ํ–ฅ์„ ์ฃผ๊ฑฐ๋‚˜ ๋ฐ›์œผ๋ฉด ์•ˆ ๋ผ. ์ˆœ์„œ์— ์ƒ๊ด€์—†์ด ์‹คํ–‰๋˜์–ด์•ผ ํ•ด!

// โŒ ๋‚˜์œ ์˜ˆ: ํ…Œ์ŠคํŠธ๊ฐ€ ์„œ๋กœ ์˜์กด
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('๋ฐ•๊ฐœ๋ฐœ');
});
โœ… 3. ๋ช…ํ™•ํ•œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„ ์ง“๊ธฐ

ํ…Œ์ŠคํŠธ ์ด๋ฆ„๋งŒ ๋ด๋„ ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด:

// โŒ ๋‚˜์œ ์˜ˆ
test('test1', () => { ... });
test('works', () => { ... });

// โœ… ์ข‹์€ ์˜ˆ
test('์œ ํšจํ•œ ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•œ๋‹ค', () => { ... });
test('์ค‘๋ณต๋œ ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ ์—๋Ÿฌ๋ฅผ ๋˜์ง„๋‹ค', () => { ... });
test('๋นˆ ์ด๋ฆ„์œผ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ ValidationError๋ฅผ ๋˜์ง„๋‹ค', () => { ... });
โœ… 4. ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜๋งŒ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ ์›์ธ์„ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋‚˜์˜ ๊ฐœ๋…๋งŒ ํ…Œ์ŠคํŠธํ•ด:

// โŒ ๋‚˜์œ ์˜ˆ: ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ํ•œ ๋ฒˆ์— ํ…Œ์ŠคํŠธ
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);
});
โœ… 5. ์—ฃ์ง€ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์ •์ƒ์ ์ธ ๊ฒฝ์šฐ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์˜ˆ์™ธ ์ƒํ™ฉ๋„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•ด:

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();
  });
  
});
โœ… 6. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌํ•˜๊ธฐ

๋ฐ˜๋ณต๋˜๋Š” ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ:

// 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 ์•ฑ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊ฒŒ. ๐ŸŽฏ

๐Ÿ“ 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;
๐Ÿงช Jest๋กœ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
// 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๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•˜๊ณ , ๊ทธ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก ์ด์•ผ. ์ฒ˜์Œ์—” ์ด์ƒํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, ์ต์ˆ™ํ•ด์ง€๋ฉด ์ •๋ง ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ๊ฐ€ ๋ผ! ๐Ÿ”ด๐ŸŸขโ™ป๏ธ

๐Ÿ”„ TDD์˜ 3๋‹จ๊ณ„ ์‚ฌ์ดํด (Red-Green-Refactor)

๐Ÿ”ด Red (์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ)
์•„์ง ๊ตฌํ˜„๋˜์ง€ ์•Š์€ ๊ธฐ๋Šฅ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•ด. ๋‹น์—ฐํžˆ ์‹คํŒจํ•˜๊ฒ ์ง€?

๐ŸŸข Green (ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋Š” ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ์ž‘์„ฑ)
ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด. ์™„๋ฒฝํ•  ํ•„์š” ์—†์–ด!

โ™ป๏ธ Refactor (์ฝ”๋“œ ๊ฐœ์„ )
ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋Š” ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋ฉด์„œ ์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•ด.

์‹ค์ œ ์˜ˆ์‹œ๋ฅผ ๋ณด์ž. ๋ฌธ์ž์—ด์„ ๋ฐ˜๋Œ€๋กœ ๋’ค์ง‘๋Š” ํ•จ์ˆ˜๋ฅผ TDD๋กœ ๋งŒ๋“ค์–ด๋ณผ๊ฒŒ:

๐Ÿ”ด 1๋‹จ๊ณ„: Red - ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
// reverse.test.js
const reverse = require('./reverse');

describe('reverse()', () => {
  test('๋ฌธ์ž์—ด์„ ๋’ค์ง‘๋Š”๋‹ค', () => {
    expect(reverse('hello')).toBe('olleh');
  });
});

์ด ์ƒํƒœ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด? ๋‹น์—ฐํžˆ ์‹คํŒจํ•ด! reverse ํ•จ์ˆ˜๊ฐ€ ์•„์ง ์—†์œผ๋‹ˆ๊นŒ. ๐Ÿ”ด

๐ŸŸข 2๋‹จ๊ณ„: Green - ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋Š” ์ฝ”๋“œ ์ž‘์„ฑ
// reverse.js
function reverse(str) {
  return str.split('').reverse().join('');
}

module.exports = reverse;

์ด์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ†ต๊ณผํ•ด! ๐ŸŸข

๐Ÿ”ด 3๋‹จ๊ณ„: ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
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');
  });
});

๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋„ค! ๐ŸŽ‰

โ™ป๏ธ 4๋‹จ๊ณ„: Refactor - ์ฝ”๋“œ ๊ฐœ์„ 

ํ˜„์žฌ ์ฝ”๋“œ๋Š” ์ด๋ฏธ ๊ฐ„๋‹จํ•˜์ง€๋งŒ, ๋งŒ์•ฝ ๋” ๋ณต์žกํ–ˆ๋‹ค๋ฉด ์ด ๋‹จ๊ณ„์—์„œ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด. ์ค‘์š”ํ•œ ๊ฑด ํ…Œ์ŠคํŠธ๊ฐ€ ๊ณ„์† ํ†ต๊ณผํ•˜๋Š” ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” ๊ฑฐ์•ผ!

๐Ÿ’ก TDD์˜ ์žฅ์ :

โ€ข ๋ช…ํ™•ํ•œ ์š”๊ตฌ์‚ฌํ•ญ: ํ…Œ์ŠคํŠธ๊ฐ€ ๊ณง ๋ช…์„ธ์„œ๊ฐ€ ๋ผ
โ€ข ๋ฒ„๊ทธ ๊ฐ์†Œ: ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „์— ์˜ˆ์™ธ ์ผ€์ด์Šค๋ฅผ ์ƒ๊ฐํ•˜๊ฒŒ ๋ผ
โ€ข ๋ฆฌํŒฉํ† ๋ง ์ž์‹ ๊ฐ: ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋‹ˆ ์•ˆ์ „ํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด
โ€ข ๋ฌธ์„œํ™”: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์‚ฌ์šฉ ์˜ˆ์‹œ๊ฐ€ ๋ผ
โ€ข ์„ค๊ณ„ ๊ฐœ์„ : ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋Š” ๋ณดํ†ต ์ข‹์€ ์„ค๊ณ„์•ผ

์ฒ˜์Œ์—” ์–ด์ƒ‰ํ•˜๊ณ  ๋А๋ฆด ์ˆ˜ ์žˆ์ง€๋งŒ, ์ต์ˆ™ํ•ด์ง€๋ฉด ์˜คํžˆ๋ ค ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ๋นจ๋ผ์ ธ! ๐Ÿ˜Š

๐ŸŒŸ ์žฌ๋Šฅ๋„ท์—์„œ ํ…Œ์ŠคํŒ… ์Šคํ‚ฌ ํ™œ์šฉํ•˜๊ธฐ

ํ…Œ์ŠคํŒ… ๋Šฅ๋ ฅ์€ ํ”„๋กœ๊ทธ๋ž˜๋จธ๋กœ์„œ ์ •๋ง ์ค‘์š”ํ•œ ์Šคํ‚ฌ์ด์•ผ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์žฌ๋Šฅ์„ ๊ฑฐ๋ž˜ํ•  ๋•Œ๋„ ํฐ ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ผ

๐ŸŽฏ ์žฌ๋Šฅ๋„ท์—์„œ ํ…Œ์ŠคํŒ… ์Šคํ‚ฌ์„ ์–ดํ•„ํ•˜๋Š” ๋ฐฉ๋ฒ•

1. ํฌํŠธํด๋ฆฌ์˜ค์— ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ํฌํ•จ
GitHub ์ €์žฅ์†Œ์— ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ•จ๊ป˜ ์˜ฌ๋ ค. ์ปค๋ฒ„๋ฆฌ์ง€ ๋ฐฐ์ง€๋„ ์ถ”๊ฐ€ํ•˜๋ฉด ๋” ์ข‹์•„!

2. ํ…Œ์ŠคํŒ…์„ ์„œ๋น„์Šค ํ•ญ๋ชฉ์— ์ถ”๊ฐ€
"๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ํฌํ•จ", "Jest/Mocha ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ œ๊ณต" ๊ฐ™์€ ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•ด๋ด.

3. ํ’ˆ์งˆ ๋ณด์ฆ ๊ฐ•์กฐ
"ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ์ด์ƒ ๋ณด์žฅ", "TDD ๋ฐฉ์‹์œผ๋กœ ๊ฐœ๋ฐœ" ๊ฐ™์€ ๋ฌธ๊ตฌ๋กœ ์‹ ๋ขฐ๋„๋ฅผ ๋†’์—ฌ.

4. ํ…Œ์ŠคํŒ… ์ปจ์„คํŒ… ์„œ๋น„์Šค
๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ํ…Œ์ŠคํŒ… ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•ด์ฃผ๋Š” ์„œ๋น„์Šค๋„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด.

5. ๊ต์œก ์ฝ˜ํ…์ธ  ์ œ์ž‘
ํ…Œ์ŠคํŒ… ํŠœํ† ๋ฆฌ์–ผ์ด๋‚˜ ๊ฐ•์˜ ์ž๋ฃŒ๋ฅผ ๋งŒ๋“ค์–ด์„œ ํŒ๋งคํ•  ์ˆ˜๋„ ์žˆ์ง€!

์‹ค์ œ๋กœ ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ ์ƒ๊ฐํ•ด๋ด. ๋‘ ๊ฐœ๋ฐœ์ž๊ฐ€ ์žˆ๋Š”๋ฐ:

๊ฐœ๋ฐœ์ž A: "์ฝ”๋“œ ์ž‘์„ฑํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค"
๊ฐœ๋ฐœ์ž B: "ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋œ ๊ณ ํ’ˆ์งˆ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค. ์ปค๋ฒ„๋ฆฌ์ง€ 80% ์ด์ƒ ๋ณด์žฅ"

๋ˆ„๊ตฌํ•œํ…Œ ๋งก๊ธฐ๊ณ  ์‹ถ์„๊นŒ? ๋‹น์—ฐํžˆ B์ง€! ๐Ÿ˜Š

ํ…Œ์ŠคํŒ… ๋Šฅ๋ ฅ์€ ๋‹จ์ˆœํžˆ ๋ฒ„๊ทธ๋ฅผ ์ค„์ด๋Š” ๊ฒƒ ์ด์ƒ์˜ ๊ฐ€์น˜๊ฐ€ ์žˆ์–ด. ์ „๋ฌธ์„ฑ๊ณผ ์‹ ๋ขฐ๋„๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ์ง€ํ‘œ๊ฐ€ ๋˜๊ฑฐ๋“ .

๐ŸŽฌ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€, ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์ง€? ๐Ÿ˜… ์šฐ๋ฆฌ๊ฐ€ ํ•จ๊ป˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณผ๊นŒ:

โœ… ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ค‘์š”์„ฑ๊ณผ ํ•„์š”์„ฑ
โœ… ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ ๊ฐœ๋…๊ณผ ํŠน์ง•
โœ… Jest์˜ ์„ค์น˜, ์„ค์ •, ์‚ฌ์šฉ๋ฒ•
โœ… Mocha + Chai + Sinon ์กฐํ•ฉ
โœ… ๋น„๋™๊ธฐ ์ฝ”๋“œ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•
โœ… Mock, Stub, Spy ํ™œ์šฉ
โœ… ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ •
โœ… ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค
โœ… TDD ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก 
โœ… ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์˜ˆ์ œ

ํ…Œ์ŠคํŒ…์€ ์ฒ˜์Œ์—” ๊ท€์ฐฎ๊ณ  ์‹œ๊ฐ„ ๋‚ญ๋น„์ฒ˜๋Ÿผ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. ๋‚˜๋„ ๊ทธ๋žฌ๊ฑฐ๋“ . ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๊ณ , ํ˜‘์—…์„ ํ•˜๊ฒŒ ๋˜๋ฉด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ์ง„๊ฐ€๋ฅผ ๋А๋ผ๊ฒŒ ๋์–ด. ๐ŸŒŸ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋ฏธ๋ž˜์˜ ๋‚˜๋ฅผ ์œ„ํ•œ ์„ ๋ฌผ์ด์•ผ. 6๊ฐœ์›” ํ›„์— ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ๋ดค์„ ๋•Œ, ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋ฉด ์ž์‹  ์žˆ๊ฒŒ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ .

์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํ•˜๊ฒŒ ํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ž‘์€ ํ•จ์ˆ˜ ํ•˜๋‚˜๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณด๊ณ , ์ ์  ๋ฒ”์œ„๋ฅผ ๋„“ํ˜€๊ฐ€๋ฉด ๋ผ. ๊ทธ๋Ÿฌ๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ์ž์—ฐ์Šค๋Ÿฌ์›Œ์งˆ ๊ฑฐ์•ผ! ๐Ÿ’ช

๊ทธ๋ฆฌ๊ณ  ๊ธฐ์–ตํ•ด. ์ข‹์€ ํ…Œ์ŠคํŠธ๋Š” ์ข‹์€ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด. ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ๋Š” ๋ณดํ†ต ์ž˜ ์„ค๊ณ„๋œ ์ฝ”๋“œ๊ฑฐ๋“ . ํ…Œ์ŠคํŒ…์„ ๋ฐฐ์šฐ๋ฉด์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋” ๋‚˜์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜๋Š” ๊ฑฐ์ง€! ๐Ÿš€

์ž, ์ด์ œ ๋‹น์‹  ์ฐจ๋ก€์•ผ! ์˜ค๋Š˜ ๋ฐฐ์šด ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด๋ด. ์ฒ˜์Œ์—” ์–ด๋ ต๊ฒ ์ง€๋งŒ, ํฌ๊ธฐํ•˜์ง€ ๋ง๊ณ  ๊ณ„์† ์—ฐ์Šตํ•˜๋ฉด ๋ถ„๋ช… ์‹ค๋ ฅ์ด ๋Š˜ ๊ฑฐ์•ผ.

ํ™”์ดํŒ…! ๐ŸŽ‰ ๊ทธ๋ฆฌ๊ณ  ๊ถ๊ธˆํ•œ ๊ฒŒ ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ๋ฌผ์–ด๋ด. ์šฐ๋ฆฌ ๋ชจ๋‘ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ๋˜์ž! ๐Ÿ˜Š

Happy Testing! ํ…Œ์ŠคํŠธ๋กœ ๋” ๋‚˜์€ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด์š” โœ“ โœ“ โœ“ Jest Mocha TDD
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0