์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽฏ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ์™€ TDD, ์ด์ œ ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ์•Œ์•„๋ณด์ž!

๐ŸŽฏ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ์™€ TDD, ์ด์ œ ์นœ๊ตฌ์ฒ˜๋Ÿผ ์‰ฝ๊ฒŒ ์•Œ์•„๋ณด์ž!

JUnit, TestNG, Mockito๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ์˜ ๋ชจ๋“  ๊ฒƒ

๐Ÿš€ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—: ์™œ ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ์™€ TDD(Test-Driven Development)์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ๐Ÿ˜Š

ํ˜น์‹œ ์ด๋Ÿฐ ๊ฒฝํ—˜ ์žˆ์–ด? ์ฝ”๋“œ๋ฅผ ์—ด์‹ฌํžˆ ์งœ๊ณ  ๋‚˜์„œ ์‹คํ–‰ํ–ˆ๋Š”๋ฐ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฒ„๊ทธ๊ฐ€ ํŠ€์–ด๋‚˜์™€์„œ ๋ฐค์ƒˆ ๋””๋ฒ„๊น…ํ•œ ์  ๋ง์ด์•ผ. ์•„๋‹ˆ๋ฉด ๋‹ค๋ฅธ ์‚ฌ๋žŒ์ด ์ง  ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ๋Š”๋ฐ, ์–ด๋””์„ ๊ฐ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ด์„œ ๋‹นํ™ฉํ–ˆ๋˜ ๊ฒฝํ—˜? ๐Ÿคฏ

๋ฐ”๋กœ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ์žฅํ•œ ๊ฒŒ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ์™€ TDD ๋ฐฉ๋ฒ•๋ก ์ด์•ผ. ์ด๊ฑด ๋‹จ์ˆœํžˆ ๋ฒ„๊ทธ๋ฅผ ์ฐพ๋Š” ๋„๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ผ, ์ฝ”๋“œ์˜ ํ’ˆ์งˆ์„ ๋†’์ด๊ณ  ๊ฐœ๋ฐœ์ž์˜ ์ž์‹ ๊ฐ์„ ํ‚ค์›Œ์ฃผ๋Š” ๊ฐ•๋ ฅํ•œ ๋ฌด๊ธฐ์ง€!

๐Ÿ’ก ํ•ต์‹ฌ ํฌ์ธํŠธ
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋ณดํ—˜์ด์•ผ. ๋‹น์žฅ์€ ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ ๊ฐ™์ง€๋งŒ, ๋‚˜์ค‘์— ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—„์ฒญ๋‚œ ์‹œ๊ฐ„ ๋‚ญ๋น„์™€ ์ŠคํŠธ๋ ˆ์Šค๋ฅผ ๋ง‰์•„์ฃผ๋Š” ์ตœ๊ณ ์˜ ํˆฌ์ž๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์–ด.
J JUnit T TestNG M Mockito ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ 3๋Œ€์žฅ ์ž๋ฐ” ๊ฐœ๋ฐœ์ž์˜ ํ•„์ˆ˜ ๋„๊ตฌ๋“ค

๐Ÿ“š TDD(Test-Driven Development)๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

TDD๋Š” ํ…Œ์ŠคํŠธ ์ฃผ๋„ ๊ฐœ๋ฐœ์ด๋ผ๊ณ  ๋ถˆ๋Ÿฌ. ๋ง ๊ทธ๋Œ€๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐœ๋ฐœ์„ ์ด๋Œ์–ด๊ฐ€๋Š” ๋ฐฉ๋ฒ•๋ก ์ด์ง€. ์ผ๋ฐ˜์ ์ธ ๊ฐœ๋ฐœ ๋ฐฉ์‹๊ณผ๋Š” ์ˆœ์„œ๊ฐ€ ์™„์ „ํžˆ ๋ฐ˜๋Œ€์•ผ! ๐Ÿ˜ฒ

๋ณดํ†ต์€ ์ด๋ ‡๊ฒŒ ๊ฐœ๋ฐœํ•˜์ž–์•„?
1๏ธโƒฃ ๊ธฐ๋Šฅ ๊ตฌํ˜„ โ†’ 2๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ โ†’ 3๏ธโƒฃ ๋ฒ„๊ทธ ๋ฐœ๊ฒฌ โ†’ 4๏ธโƒฃ ์ˆ˜์ •

ํ•˜์ง€๋งŒ TDD๋Š” ์ด๋ ‡๊ฒŒ ํ•ด:
1๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ โ†’ 2๏ธโƒฃ ํ…Œ์ŠคํŠธ ์‹คํŒจ ํ™•์ธ โ†’ 3๏ธโƒฃ ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ ๊ตฌํ˜„ โ†’ 4๏ธโƒฃ ํ…Œ์ŠคํŠธ ํ†ต๊ณผ โ†’ 5๏ธโƒฃ ๋ฆฌํŒฉํ† ๋ง

๐Ÿ”„ TDD์˜ ํ•ต์‹ฌ ์‚ฌ์ดํด: Red-Green-Refactor

TDD์˜ ํ•ต์‹ฌ์€ Red-Green-Refactor ์‚ฌ์ดํด์ด์•ผ. ์ด๊ฒŒ ๋ญ๋ƒ๋ฉด:

๐Ÿ”ด Red (์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ)
๋จผ์ € ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด. "์ด ๊ธฐ๋Šฅ์€ ์ด๋ ‡๊ฒŒ ๋™์ž‘ํ•ด์•ผ ํ•ด!"๋ผ๋Š” ๋ช…์„ธ๋ฅผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๊ฑฐ์ง€. ๋‹น์—ฐํžˆ ์•„์ง ๊ตฌํ˜„ ์ฝ”๋“œ๊ฐ€ ์—†์œผ๋‹ˆ๊นŒ ํ…Œ์ŠคํŠธ๋Š” ์‹คํŒจํ•  ๊ฑฐ์•ผ.

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

๐Ÿ”ต Refactor (์ฝ”๋“œ ๊ฐœ์„ )
ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ–ˆ๋‹ค๋ฉด, ์ด์ œ ์ฝ”๋“œ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•  ์‹œ๊ฐ„์ด์•ผ. ์ค‘๋ณต ์ œ๊ฑฐ, ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ, ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋“ฑ์„ ์ง„ํ–‰ํ•˜๋˜, ํ…Œ์ŠคํŠธ๋Š” ๊ณ„์† ํ†ต๊ณผํ•ด์•ผ ํ•ด.

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

๐Ÿค” TDD์˜ ๋‹จ์ ๊ณผ ํ•œ๊ณ„

๋ฌผ๋ก  TDD๊ฐ€ ๋งŒ๋Šฅ์€ ์•„๋‹ˆ์•ผ. ์†”์งํžˆ ๋งํ•˜๋ฉด ์ด๋Ÿฐ ์–ด๋ ค์›€๋“ค์ด ์žˆ์–ด:

โฐ ์ดˆ๊ธฐ ๊ฐœ๋ฐœ ์‹œ๊ฐ„ ์ฆ๊ฐ€
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•˜๋‹ˆ๊นŒ ๋‹น์—ฐํžˆ ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ ค. ํŠนํžˆ TDD์— ์ต์ˆ™ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ํ™• ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์–ด.

๐Ÿ“ˆ ํ•™์Šต ๊ณก์„ 
TDD๋Š” ๋‹จ์ˆœํžˆ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ๋ฒ•๋งŒ ๋ฐฐ์šฐ๋ฉด ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ์•ผ. ์‚ฌ๊ณ ๋ฐฉ์‹ ์ž์ฒด๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•˜๊ฑฐ๋“ . ์ด๊ฒŒ ์ƒ๊ฐ๋ณด๋‹ค ์‰ฝ์ง€ ์•Š์•„.

๐ŸŽจ UI/UX ํ…Œ์ŠคํŠธ์˜ ์–ด๋ ค์›€
์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋‚˜ ๋””์ž์ธ ๊ด€๋ จ ๋ถ€๋ถ„์€ TDD๋กœ ์ ‘๊ทผํ•˜๊ธฐ๊ฐ€ ๊นŒ๋‹ค๋กœ์›Œ. ์‹œ๊ฐ์ ์ธ ๋ถ€๋ถ„์„ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ๋กœ ๊ฒ€์ฆํ•˜๊ธฐ๋Š” ์–ด๋ ต๊ฑฐ๋“ .

๐Ÿ”ง ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ ์ ์šฉ ๋‚œ์ด๋„
์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ํ”„๋กœ์ ํŠธ์— TDD๋ฅผ ์ ์šฉํ•˜๋ ค๋ฉด ์—„์ฒญ๋‚œ ๋…ธ๋ ฅ์ด ํ•„์š”ํ•ด. ๊ธฐ์กด ์ฝ”๋“œ๋ฅผ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ๊ฐ€ ํฐ ์‚ฐ์ด์•ผ.

โ˜• JUnit: ์ž๋ฐ” ํ…Œ์ŠคํŒ…์˜ ํ‘œ์ค€

JUnit์€ ์ž๋ฐ” ์ง„์˜์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. 2000๋…„์— ์ฒ˜์Œ ๋“ฑ์žฅํ•œ ์ดํ›„๋กœ ์ง€๊ธˆ๊นŒ์ง€ ์ž๋ฐ” ๊ฐœ๋ฐœ์ž๋“ค์˜ ํ•„์ˆ˜ ๋„๊ตฌ๋กœ ์ž๋ฆฌ์žก์•˜์ง€. ํ˜„์žฌ๋Š” JUnit 5(JUnit Jupiter)๊ฐ€ ์ตœ์‹  ๋ฒ„์ „์ด์•ผ. ๐ŸŽ‰

๐ŸŒŸ JUnit์˜ ์ฃผ์š” ํŠน์ง•

1. ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์˜ ์ง๊ด€์ ์ธ ๊ตฌ์กฐ
JUnit์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ด. ์ฝ”๋“œ๋งŒ ๋ด๋„ ๋ญํ•˜๋Š” ๊ฑด์ง€ ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ์–ด.

2. ํ’๋ถ€ํ•œ Assertion ๋ฉ”์„œ๋“œ
๋‹ค์–‘ํ•œ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด์„œ ๊ฑฐ์˜ ๋ชจ๋“  ์ƒํ™ฉ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์–ด.

3. ํ…Œ์ŠคํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ
ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ „ํ›„๋กœ ํ•„์š”ํ•œ ์ž‘์—…๋“ค์„ ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด.

4. ํŒŒ๋ผ๋ฏธํ„ฐํ™” ํ…Œ์ŠคํŠธ
๊ฐ™์€ ํ…Œ์ŠคํŠธ๋ฅผ ์—ฌ๋Ÿฌ ์ž…๋ ฅ๊ฐ’์œผ๋กœ ๋ฐ˜๋ณต ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ’ป JUnit 5 ๊ธฐ๋ณธ ๊ตฌ์กฐ

JUnit 5๋Š” ์„ธ ๊ฐ€์ง€ ์ฃผ์š” ๋ชจ๋“ˆ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์–ด:

JUnit Platform: ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ธฐ๋ฐ˜ ํ”Œ๋žซํผ
JUnit Jupiter: JUnit 5์˜ ์ƒˆ๋กœ์šด ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ๊ณผ ํ™•์žฅ ๋ชจ๋ธ
JUnit Vintage: JUnit 3, 4๋กœ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ์—”์ง„

๐Ÿ“ JUnit ์ฃผ์š” ์–ด๋…ธํ…Œ์ด์…˜

@Test
ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์–ด๋…ธํ…Œ์ด์…˜์ด์•ผ. ์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ๋ฉ”์„œ๋“œ๋Š” ํ…Œ์ŠคํŠธ๋กœ ์ธ์‹๋ผ.

@BeforeEach / @AfterEach
๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ ์ „ํ›„์— ์‹คํ–‰๋  ๋ฉ”์„œ๋“œ๋ฅผ ์ง€์ •ํ•ด. ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ํ•„์š”ํ•œ ์ดˆ๊ธฐํ™”๋‚˜ ์ •๋ฆฌ ์ž‘์—…์„ ํ•  ๋•Œ ์‚ฌ์šฉํ•ด.

@BeforeAll / @AfterAll
๋ชจ๋“  ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์‹คํ–‰ ์ „ํ›„์— ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋ผ. ์ฃผ๋กœ ๋น„์šฉ์ด ํฐ ๋ฆฌ์†Œ์Šค ์ดˆ๊ธฐํ™”์— ์‚ฌ์šฉํ•ด. static ๋ฉ”์„œ๋“œ์—ฌ์•ผ ํ•ด.

@DisplayName
ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋‚˜ ๋ฉ”์„œ๋“œ์— ํ‘œ์‹œ๋  ์ด๋ฆ„์„ ์ง€์ •ํ•ด. ํ•œ๊ธ€๋กœ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์„œ ๊ฐ€๋…์„ฑ์ด ์ข‹์•„.

@Disabled
ํŠน์ • ํ…Œ์ŠคํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•  ๋•Œ ์‚ฌ์šฉํ•ด. ์ผ์‹œ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์‹ถ์„ ๋•Œ ์œ ์šฉํ•ด.

@ParameterizedTest
ํŒŒ๋ผ๋ฏธํ„ฐํ™”๋œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์‚ฌ์šฉํ•ด. ์—ฌ๋Ÿฌ ์ž…๋ ฅ๊ฐ’์œผ๋กœ ๊ฐ™์€ ํ…Œ์ŠคํŠธ๋ฅผ ๋ฐ˜๋ณต ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

๐Ÿ” JUnit ์‹ค์ „ ์˜ˆ์ œ

๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ๊ธฐ ํด๋ž˜์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ๋ณผ๊นŒ? ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๊ฐ์„ ์žก์„ ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ.

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("๊ณ„์‚ฐ๊ธฐ ํ…Œ์ŠคํŠธ")
class CalculatorTest {
    
    private Calculator calculator;
    
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
        System.out.println("ํ…Œ์ŠคํŠธ ์ค€๋น„ ์™„๋ฃŒ!");
    }
    
    @Test
    @DisplayName("๋‘ ์ˆซ์ž์˜ ๋ง์…ˆ์ด ์ •ํ™•ํ•œ์ง€ ํ™•์ธ")
    void testAddition() {
        // Given (์ค€๋น„)
        int a = 5;
        int b = 3;
        
        // When (์‹คํ–‰)
        int result = calculator.add(a, b);
        
        // Then (๊ฒ€์ฆ)
        assertEquals(8, result, "5 + 3์€ 8์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค");
    }
    
    @Test
    @DisplayName("0์œผ๋กœ ๋‚˜๋ˆ„๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธ")
    void testDivisionByZero() {
        assertThrows(ArithmeticException.class, () -> {
            calculator.divide(10, 0);
        }, "0์œผ๋กœ ๋‚˜๋ˆ„๋ฉด ArithmeticException์ด ๋ฐœ์ƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค");
    }
    
    @ParameterizedTest
    @DisplayName("์—ฌ๋Ÿฌ ์ˆซ์ž ์กฐํ•ฉ์˜ ๊ณฑ์…ˆ ํ…Œ์ŠคํŠธ")
    @CsvSource({
        "2, 3, 6",
        "5, 4, 20",
        "10, 0, 0"
    })
    void testMultiplication(int a, int b, int expected) {
        assertEquals(expected, calculator.multiply(a, b));
    }
    
    @AfterEach
    void tearDown() {
        System.out.println("ํ…Œ์ŠคํŠธ ์ •๋ฆฌ ์™„๋ฃŒ!");
    }
}

์œ„ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ํ…Œ์ŠคํŠธ์˜ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์ด์ง€? Given-When-Then ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์ด ํ›จ์”ฌ ์ข‹์•„์ ธ. ์ด๊ฑด TDD์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด์ด์•ผ.

Given: ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ค€๋น„ ๋‹จ๊ณ„
When: ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธํ•  ๋™์ž‘์„ ์‹คํ–‰
Then: ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆ

๐ŸŽฏ JUnit Assertions ์™„๋ฒฝ ๊ฐ€์ด๋“œ

JUnit์˜ Assertions ํด๋ž˜์Šค๋Š” ๋‹ค์–‘ํ•œ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด. ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋“ค์„ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ:

assertEquals(expected, actual)
๋‘ ๊ฐ’์ด ๊ฐ™์€์ง€ ํ™•์ธํ•ด. ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ด๊ณ  ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ์•ผ.

assertNotEquals(unexpected, actual)
๋‘ ๊ฐ’์ด ๋‹ค๋ฅธ์ง€ ํ™•์ธํ•ด.

assertTrue(condition) / assertFalse(condition)
์กฐ๊ฑด์ด ์ฐธ์ธ์ง€ ๊ฑฐ์ง“์ธ์ง€ ํ™•์ธํ•ด.

assertNull(object) / assertNotNull(object)
๊ฐ์ฒด๊ฐ€ null์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธํ•ด.

assertThrows(exceptionClass, executable)
ํŠน์ • ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธํ•ด. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ์— ํ•„์ˆ˜์•ผ.

assertTimeout(duration, executable)
์ฝ”๋“œ๊ฐ€ ์ง€์ •๋œ ์‹œ๊ฐ„ ๋‚ด์— ์‹คํ–‰๋˜๋Š”์ง€ ํ™•์ธํ•ด. ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ์— ์œ ์šฉํ•ด.

assertAll(executables...)
์—ฌ๋Ÿฌ assertion์„ ๊ทธ๋ฃนํ™”ํ•ด์„œ ์‹คํ–‰ํ•ด. ํ•˜๋‚˜๊ฐ€ ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€๋„ ๋ชจ๋‘ ์‹คํ–‰๋ผ.
@Test
@DisplayName("assertAll์„ ์‚ฌ์šฉํ•œ ๊ทธ๋ฃน ๊ฒ€์ฆ")
void testPersonDetails() {
    Person person = new Person("ํ™๊ธธ๋™", 30, "hong@example.com");
    
    assertAll("person ์ •๋ณด ๊ฒ€์ฆ",
        () -> assertEquals("ํ™๊ธธ๋™", person.getName()),
        () -> assertEquals(30, person.getAge()),
        () -> assertTrue(person.getEmail().contains("@")),
        () -> assertNotNull(person.getId())
    );
}

๐Ÿš€ JUnit 5์˜ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๋“ค

JUnit 5๋Š” ์ด์ „ ๋ฒ„์ „์— ๋น„ํ•ด ๋งŽ์€ ๊ฐœ์„ ์ด ์žˆ์—ˆ์–ด. ํŠนํžˆ ์ฃผ๋ชฉํ•  ๋งŒํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์†Œ๊ฐœํ• ๊ฒŒ:

1. ๋™์  ํ…Œ์ŠคํŠธ (Dynamic Tests)
๋Ÿฐํƒ€์ž„์— ํ…Œ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด. ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๊ฐ€ ๋™์ ์œผ๋กœ ๊ฒฐ์ •๋  ๋•Œ ์œ ์šฉํ•ด.

@TestFactory
@DisplayName("๋™์  ํ…Œ์ŠคํŠธ ์ƒ์„ฑ")
Stream<DynamicTest> dynamicTestsFromStream() {
    return Stream.of(2, 4, 6, 8, 10)
        .map(number -> DynamicTest.dynamicTest(
            "์ง์ˆ˜ ํ…Œ์ŠคํŠธ: " + number,
            () -> assertTrue(number % 2 == 0)
        ));
}

2. ์ค‘์ฒฉ ํ…Œ์ŠคํŠธ (Nested Tests)
ํ…Œ์ŠคํŠธ๋ฅผ ๊ณ„์ธต์ ์œผ๋กœ ๊ตฌ์กฐํ™”ํ•  ์ˆ˜ ์žˆ์–ด. ๊ด€๋ จ๋œ ํ…Œ์ŠคํŠธ๋“ค์„ ๊ทธ๋ฃนํ™”ํ•˜๊ธฐ ์ข‹์•„.

@DisplayName("์‡ผํ•‘์นดํŠธ ํ…Œ์ŠคํŠธ")
class ShoppingCartTest {
    
    @Nested
    @DisplayName("๋นˆ ์นดํŠธ์ผ ๋•Œ")
    class WhenEmpty {
        
        @Test
        @DisplayName("์ด์•ก์€ 0์ด์–ด์•ผ ํ•œ๋‹ค")
        void totalShouldBeZero() {
            ShoppingCart cart = new ShoppingCart();
            assertEquals(0, cart.getTotal());
        }
    }
    
    @Nested
    @DisplayName("์ƒํ’ˆ์ด ์žˆ์„ ๋•Œ")
    class WhenHasItems {
        
        @Test
        @DisplayName("์ด์•ก์€ ์ƒํ’ˆ ๊ฐ€๊ฒฉ์˜ ํ•ฉ์ด์–ด์•ผ ํ•œ๋‹ค")
        void totalShouldBeSumOfItems() {
            ShoppingCart cart = new ShoppingCart();
            cart.addItem(new Item("์ฑ…", 15000));
            cart.addItem(new Item("ํŽœ", 1000));
            assertEquals(16000, cart.getTotal());
        }
    }
}

3. ์กฐ๊ฑด๋ถ€ ํ…Œ์ŠคํŠธ ์‹คํ–‰
ํŠน์ • ์กฐ๊ฑด์—์„œ๋งŒ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜ ๊ฑด๋„ˆ๋›ธ ์ˆ˜ ์žˆ์–ด.

@Test
@EnabledOnOs(OS.WINDOWS)
@DisplayName("์œˆ๋„์šฐ์—์„œ๋งŒ ์‹คํ–‰")
void testOnWindows() {
    // ์œˆ๋„์šฐ ์ „์šฉ ํ…Œ์ŠคํŠธ
}

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "production")
@DisplayName("ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‹คํ–‰")
void testOnProduction() {
    // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ ํ…Œ์ŠคํŠธ
}

@Test
@EnabledIf("customCondition")
@DisplayName("์ปค์Šคํ…€ ์กฐ๊ฑด์ด ์ฐธ์ผ ๋•Œ๋งŒ ์‹คํ–‰")
void testWithCustomCondition() {
    // ์กฐ๊ฑด๋ถ€ ํ…Œ์ŠคํŠธ
}

boolean customCondition() {
    return LocalDate.now().getDayOfWeek() == DayOfWeek.MONDAY;
}
TDD ๊ฐœ๋ฐœ ์‚ฌ์ดํด RED ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ GREEN ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ์ฝ”๋“œ ์ž‘์„ฑ REFACTOR ์ฝ”๋“œ ๋ฆฌํŒฉํ† ๋ง 1๋‹จ๊ณ„ 2๋‹จ๊ณ„ 3๋‹จ๊ณ„ ๋ฐ˜๋ณต์ ์ธ ์‚ฌ์ดํด๋กœ ์ฝ”๋“œ ํ’ˆ์งˆ ํ–ฅ์ƒ ์ž‘์€ ๋‹จ์œ„๋กœ ๋น ๋ฅด๊ฒŒ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์œผ๋ฉฐ ๊ฐœ๋ฐœํ•˜์„ธ์š”

๐ŸŽญ TestNG: ๋” ๊ฐ•๋ ฅํ•œ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ

TestNG๋Š” "Testing Next Generation"์˜ ์•ฝ์ž์•ผ. JUnit์—์„œ ์˜๊ฐ์„ ๋ฐ›์•„ ๋งŒ๋“ค์–ด์กŒ์ง€๋งŒ, ๋” ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•ด. ํŠนํžˆ ๋Œ€๊ทœ๋ชจ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๊ฑฐ๋‚˜ ๋ณต์žกํ•œ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ๋น›์„ ๋ฐœํ•ด. ๐ŸŒŸ

๐Ÿ†š JUnit vs TestNG: ๋ฌด์—‡์ด ๋‹ค๋ฅผ๊นŒ?

๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด "JUnit์ด ์žˆ๋Š”๋ฐ ์™œ TestNG๋ฅผ ์จ์•ผ ํ•˜๋‚˜์š”?"๋ผ๊ณ  ๋ฌผ์–ด๋ด. ์ข‹์€ ์งˆ๋ฌธ์ด์•ผ! ๋‘˜ ๋‹ค ํ›Œ๋ฅญํ•œ ํ”„๋ ˆ์ž„์›Œํฌ์ง€๋งŒ, ๊ฐ์ž์˜ ๊ฐ•์ ์ด ์žˆ์–ด.

TestNG์˜ ์ฃผ์š” ์žฅ์ 

1๏ธโƒฃ ๋” ์œ ์—ฐํ•œ ํ…Œ์ŠคํŠธ ์„ค์ •
TestNG๋Š” XML ํŒŒ์ผ์ด๋‚˜ ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๊ทธ๋ฃนํ™”ํ•˜๊ณ  ์ˆœ์„œ๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด. ๋ณต์žกํ•œ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฌ์›Œ.

2๏ธโƒฃ ์˜์กด์„ฑ ํ…Œ์ŠคํŠธ
ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜์กดํ•˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•ด์•ผ๋งŒ ํ”„๋กœํ•„ ์ˆ˜์ • ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ์‹์ด์ง€.

3๏ธโƒฃ ๋ณ‘๋ ฌ ์‹คํ–‰
ํ…Œ์ŠคํŠธ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•ด์„œ ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹œ๊ฐ„์„ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์–ด. ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ์—์„œ ์ •๋ง ์œ ์šฉํ•ด.

4๏ธโƒฃ ๋ฐ์ดํ„ฐ ํ”„๋กœ๋ฐ”์ด๋”
๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์„ธํŠธ๋ฅผ ํ…Œ์ŠคํŠธ์— ์ œ๊ณตํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์žˆ์–ด.

5๏ธโƒฃ ๋ฆฌ์Šค๋„ˆ์™€ ๋ฆฌํฌํŒ…
ํ…Œ์ŠคํŠธ ์‹คํ–‰ ๊ณผ์ •์„ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ์ปค์Šคํ…€ ๋ฆฌํฌํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์‰ฌ์›Œ.

๐Ÿ“‹ TestNG ์ฃผ์š” ์–ด๋…ธํ…Œ์ด์…˜

TestNG์˜ ์–ด๋…ธํ…Œ์ด์…˜์€ JUnit๊ณผ ๋น„์Šทํ•˜์ง€๋งŒ ๋” ๋งŽ์€ ์˜ต์…˜์„ ์ œ๊ณตํ•ด:

import org.testng.annotations.*;
import static org.testng.Assert.*;

public class TestNGExample {
    
    @BeforeSuite
    public void beforeSuite() {
        System.out.println("์ „์ฒด ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ์‹œ์ž‘ ์ „ ์‹คํ–‰");
    }
    
    @BeforeTest
    public void beforeTest() {
        System.out.println("ํ…Œ์ŠคํŠธ ํƒœ๊ทธ ์‹œ์ž‘ ์ „ ์‹คํ–‰");
    }
    
    @BeforeClass
    public void beforeClass() {
        System.out.println("ํ˜„์žฌ ํด๋ž˜์Šค์˜ ์ฒซ ํ…Œ์ŠคํŠธ ์ „ ์‹คํ–‰");
    }
    
    @BeforeMethod
    public void beforeMethod() {
        System.out.println("๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ์ „ ์‹คํ–‰");
    }
    
    @Test(priority = 1)
    public void testLogin() {
        System.out.println("๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ - ์šฐ์„ ์ˆœ์œ„ 1");
        assertTrue(true);
    }
    
    @Test(priority = 2, dependsOnMethods = {"testLogin"})
    public void testDashboard() {
        System.out.println("๋Œ€์‹œ๋ณด๋“œ ํ…Œ์ŠคํŠธ - ๋กœ๊ทธ์ธ ํ›„ ์‹คํ–‰");
        assertTrue(true);
    }
    
    @AfterMethod
    public void afterMethod() {
        System.out.println("๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ ํ›„ ์‹คํ–‰");
    }
    
    @AfterClass
    public void afterClass() {
        System.out.println("ํ˜„์žฌ ํด๋ž˜์Šค์˜ ๋งˆ์ง€๋ง‰ ํ…Œ์ŠคํŠธ ํ›„ ์‹คํ–‰");
    }
    
    @AfterTest
    public void afterTest() {
        System.out.println("ํ…Œ์ŠคํŠธ ํƒœ๊ทธ ์ข…๋ฃŒ ํ›„ ์‹คํ–‰");
    }
    
    @AfterSuite
    public void afterSuite() {
        System.out.println("์ „์ฒด ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ์ข…๋ฃŒ ํ›„ ์‹คํ–‰");
    }
}

๋ณด๋‹ค์‹œํ”ผ TestNG๋Š” ํ…Œ์ŠคํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๋” ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด. @BeforeSuite์™€ @AfterSuite๋Š” ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ์‹œ์ž‘๊ณผ ๋์— ํ•œ ๋ฒˆ์”ฉ๋งŒ ์‹คํ–‰๋ผ. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์ด๋‚˜ ์„œ๋ฒ„ ์‹œ์ž‘ ๊ฐ™์€ ๋น„์šฉ์ด ํฐ ์ž‘์—…์— ๋”ฑ์ด์•ผ!

๐ŸŽฏ TestNG์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค

1. ๊ทธ๋ฃน ํ…Œ์ŠคํŠธ
ํ…Œ์ŠคํŠธ๋ฅผ ๋…ผ๋ฆฌ์ ์ธ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด์„œ ์„ ํƒ์ ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด.

@Test(groups = {"smoke", "regression"})
public void testCriticalFeature() {
    // ์ค‘์š”ํ•œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
}

@Test(groups = {"regression"})
public void testDetailedFeature() {
    // ์ƒ์„ธํ•œ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
}

@Test(groups = {"smoke"})
public void testBasicFeature() {
    // ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ
}

XML ์„ค์ • ํŒŒ์ผ์—์„œ ํŠน์ • ๊ทธ๋ฃน๋งŒ ์‹คํ–‰ํ•˜๋„๋ก ์ง€์ •ํ•  ์ˆ˜ ์žˆ์–ด:

<suite name="Test Suite">
    <test name="Smoke Tests">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="com.example.TestClass"/>
        </classes>
    </test>
</suite>

2. ๋ฐ์ดํ„ฐ ํ”„๋กœ๋ฐ”์ด๋” (Data Provider)
๊ฐ™์€ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋กœ ๋ฐ˜๋ณต ์‹คํ–‰ํ•  ๋•Œ ์ •๋ง ์œ ์šฉํ•ด. JUnit์˜ ํŒŒ๋ผ๋ฏธํ„ฐํ™” ํ…Œ์ŠคํŠธ๋ณด๋‹ค ํ›จ์”ฌ ๊ฐ•๋ ฅํ•ด!

@DataProvider(name = "loginData")
public Object[][] createLoginData() {
    return new Object[][] {
        {"user1@example.com", "password123", true},
        {"user2@example.com", "wrongpass", false},
        {"invalid@email", "password123", false},
        {"", "", false}
    };
}

@Test(dataProvider = "loginData")
public void testLogin(String email, String password, boolean expectedResult) {
    LoginService service = new LoginService();
    boolean result = service.login(email, password);
    assertEquals(result, expectedResult, 
        "๋กœ๊ทธ์ธ ๊ฒฐ๊ณผ๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค: " + email);
}

๋ฐ์ดํ„ฐ ํ”„๋กœ๋ฐ”์ด๋”๋Š” ๋ฉ”์„œ๋“œ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ํŒŒ์ผ์ด๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์–ด. ์‹ค์ „์—์„œ ์ •๋ง ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์ด์•ผ!

3. ์˜์กด์„ฑ ํ…Œ์ŠคํŠธ
TestNG์˜ ๋…ํŠนํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์•ผ. ํ…Œ์ŠคํŠธ ๊ฐ„์˜ ์˜์กด ๊ด€๊ณ„๋ฅผ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ์–ด.

@Test
public void serverStartedOk() {
    // ์„œ๋ฒ„ ์‹œ์ž‘ ํ…Œ์ŠคํŠธ
    assertTrue(server.isRunning());
}

@Test(dependsOnMethods = {"serverStartedOk"})
public void testDatabaseConnection() {
    // ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ผ ๋•Œ๋งŒ DB ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ
    assertTrue(database.isConnected());
}

@Test(dependsOnMethods = {"testDatabaseConnection"})
public void testDataRetrieval() {
    // DB ์—ฐ๊ฒฐ์ด ์„ฑ๊ณตํ–ˆ์„ ๋•Œ๋งŒ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ…Œ์ŠคํŠธ
    assertNotNull(database.getData());
}

๋งŒ์•ฝ serverStartedOk ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋ฉด, ์˜์กดํ•˜๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ๋“ค์€ ์ž๋™์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ์–ด์ ธ. ๋ถˆํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ ์‹คํ–‰์„ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด์„œ ํšจ์œจ์ ์ด์•ผ!

4. ๋ณ‘๋ ฌ ์‹คํ–‰
TestNG๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ๋ณ‘๋ ฌ๋กœ ์‹คํ–‰ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด. XML ์„ค์ •๋งŒ์œผ๋กœ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์–ด.

<suite name="Parallel Suite" parallel="methods" thread-count="5">
    <test name="Parallel Test">
        <classes>
            <class name="com.example.TestClass1"/>
            <class name="com.example.TestClass2"/>
        </classes>
    </test>
</suite>

parallel ์†์„ฑ์œผ๋กœ ๋ณ‘๋ ฌํ™” ์ˆ˜์ค€์„ ์กฐ์ ˆํ•  ์ˆ˜ ์žˆ์–ด:
- methods: ๋ฉ”์„œ๋“œ ๋‹จ์œ„๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰
- classes: ํด๋ž˜์Šค ๋‹จ์œ„๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰
- tests: ํ…Œ์ŠคํŠธ ํƒœ๊ทธ ๋‹จ์œ„๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰
- instances: ์ธ์Šคํ„ด์Šค ๋‹จ์œ„๋กœ ๋ณ‘๋ ฌ ์‹คํ–‰

๐Ÿ“Š TestNG ๋ฆฌํฌํŒ…

TestNG๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ HTML ๋ฆฌํฌํŠธ๋ฅผ ์ƒ์„ฑํ•ด์ค˜. ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•˜๊ธฐ ์ •๋ง ์ข‹์•„. ๊ฒŒ๋‹ค๊ฐ€ ์ปค์Šคํ…€ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋งŒ๋“ค์–ด์„œ ์›ํ•˜๋Š” ํ˜•ํƒœ์˜ ๋ฆฌํฌํŠธ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜๋„ ์žˆ์–ด.

public class CustomListener implements ITestListener {
    
    @Override
    public void onTestStart(ITestResult result) {
        System.out.println("ํ…Œ์ŠคํŠธ ์‹œ์ž‘: " + result.getName());
    }
    
    @Override
    public void onTestSuccess(ITestResult result) {
        System.out.println("ํ…Œ์ŠคํŠธ ์„ฑ๊ณต: " + result.getName());
    }
    
    @Override
    public void onTestFailure(ITestResult result) {
        System.out.println("ํ…Œ์ŠคํŠธ ์‹คํŒจ: " + result.getName());
        // ์Šคํฌ๋ฆฐ์ƒท ์บก์ฒ˜, ๋กœ๊ทธ ์ €์žฅ ๋“ฑ
    }
    
    @Override
    public void onTestSkipped(ITestResult result) {
        System.out.println("ํ…Œ์ŠคํŠธ ๊ฑด๋„ˆ๋œ€: " + result.getName());
    }
}

๋ฆฌ์Šค๋„ˆ๋ฅผ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์— ์ ์šฉํ•˜๋ ค๋ฉด:

@Listeners(CustomListener.class)
public class MyTest {
    // ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋“ค
}

๐ŸŽช Mockito: ๋ชจํ‚น์˜ ๋งˆ๋ฒ•์‚ฌ

์ž, ์ด์ œ ์ •๋ง ์žฌ๋ฏธ์žˆ๋Š” ๋ถ€๋ถ„์œผ๋กœ ์™”์–ด! Mockito๋Š” ๋ชจํ‚น(Mocking) ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. "๋ชจํ‚น์ด ๋ญ๋ƒ๊ณ ?" ์ข‹์€ ์งˆ๋ฌธ์ด์•ผ! ๐Ÿ˜Š

์‹ค์ œ ๊ฐœ๋ฐœ์—์„œ๋Š” ํ…Œ์ŠคํŠธํ•˜๋ ค๋Š” ์ฝ”๋“œ๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์™ธ๋ถ€ API, ํŒŒ์ผ ์‹œ์Šคํ…œ ๊ฐ™์€ ์™ธ๋ถ€ ์˜์กด์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. ์ด๋Ÿฐ ๊ฑธ ์‹ค์ œ๋กœ ์—ฐ๊ฒฐํ•ด์„œ ํ…Œ์ŠคํŠธํ•˜๋ฉด ์—ฌ๋Ÿฌ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒจ:

โŒ ํ…Œ์ŠคํŠธ๊ฐ€ ๋А๋ ค์ ธ
โŒ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์ ธ
โŒ ๋น„์šฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด (API ํ˜ธ์ถœ ๋น„์šฉ ๋“ฑ)
โŒ ํŠน์ • ์ƒํ™ฉ(์—๋Ÿฌ ์ƒํ™ฉ ๋“ฑ)์„ ์žฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์›Œ

๋ฐ”๋กœ ์ด๋Ÿด ๋•Œ Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑฐ์•ผ! Mock ๊ฐ์ฒด๋Š” ์‹ค์ œ ๊ฐ์ฒด๋ฅผ ํ‰๋‚ด๋‚ด๋Š” ๊ฐ€์งœ ๊ฐ์ฒด์ธ๋ฐ, ์šฐ๋ฆฌ๊ฐ€ ์›ํ•˜๋Š” ๋Œ€๋กœ ๋™์ž‘ํ•˜๋„๋ก ํ”„๋กœ๊ทธ๋ž˜๋ฐํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŽญ

๐ŸŒŸ Mockito์˜ ํ•ต์‹ฌ ๊ฐœ๋…

1. Mock ๊ฐ์ฒด ์ƒ์„ฑ
Mockito๋กœ Mock ๊ฐ์ฒด๋ฅผ ๋งŒ๋“œ๋Š” ๊ฑด ์ •๋ง ๊ฐ„๋‹จํ•ด.

import static org.mockito.Mockito.*;

// ๋ฐฉ๋ฒ• 1: mock() ๋ฉ”์„œ๋“œ ์‚ฌ์šฉ
UserRepository mockRepository = mock(UserRepository.class);

// ๋ฐฉ๋ฒ• 2: @Mock ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ
@Mock
private UserRepository mockRepository;

// @Mock์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์ดˆ๊ธฐํ™”๊ฐ€ ํ•„์š”ํ•ด
@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
}

2. Stubbing (๋™์ž‘ ์ •์˜)
Mock ๊ฐ์ฒด๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ• ์ง€ ์ •์˜ํ•˜๋Š” ๊ฑฐ์•ผ. "์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด ์ด๋Ÿฐ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด!"๋ผ๊ณ  ์ง€์ •ํ•˜๋Š” ๊ฑฐ์ง€.

// ํŠน์ • ์ธ์ž๋กœ ํ˜ธ์ถœ๋˜๋ฉด ํŠน์ • ๊ฐ’ ๋ฐ˜ํ™˜
when(mockRepository.findById(1L))
    .thenReturn(new User(1L, "ํ™๊ธธ๋™"));

// ์–ด๋–ค ์ธ์ž๋กœ ํ˜ธ์ถœ๋˜๋“  ๊ฐ™์€ ๊ฐ’ ๋ฐ˜ํ™˜
when(mockRepository.findById(anyLong()))
    .thenReturn(new User(1L, "ํ™๊ธธ๋™"));

// ์˜ˆ์™ธ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ
when(mockRepository.findById(999L))
    .thenThrow(new UserNotFoundException("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"));

// ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœ๋  ๋•Œ ๋‹ค๋ฅธ ๊ฐ’ ๋ฐ˜ํ™˜
when(mockRepository.count())
    .thenReturn(10L)
    .thenReturn(20L)
    .thenReturn(30L);

3. Verification (๊ฒ€์ฆ)
Mock ๊ฐ์ฒด์˜ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹ค์ œ๋กœ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€, ๋ช‡ ๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด.

// ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
verify(mockRepository).findById(1L);

// ์ •ํ™•ํžˆ n๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
verify(mockRepository, times(3)).findAll();

// ์ตœ์†Œ n๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
verify(mockRepository, atLeast(1)).save(any(User.class));

// ์ตœ๋Œ€ n๋ฒˆ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
verify(mockRepository, atMost(5)).delete(any(User.class));

// ํ•œ ๋ฒˆ๋„ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ
verify(mockRepository, never()).deleteAll();

// ํ˜ธ์ถœ ์ˆœ์„œ ๊ฒ€์ฆ
InOrder inOrder = inOrder(mockRepository);
inOrder.verify(mockRepository).findById(1L);
inOrder.verify(mockRepository).save(any(User.class));

๐Ÿ’ก Mockito ์‹ค์ „ ์˜ˆ์ œ

์ด๋ก ๋งŒ ๋ณด๋ฉด ์ข€ ์ถ”์ƒ์ ์ด์ง€? ์‹ค์ œ ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด Mockito๊ฐ€ ์–ผ๋งˆ๋‚˜ ์œ ์šฉํ•œ์ง€ ๋ณด์—ฌ์ค„๊ฒŒ!

์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋ฅผ ํ…Œ์ŠคํŠธํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. ์ด ์„œ๋น„์Šค๋Š” UserRepository์— ์˜์กดํ•˜๊ณ  ์žˆ์–ด.

// ํ…Œ์ŠคํŠธํ•  ์‹ค์ œ ํด๋ž˜์Šค
public class UserService {
    private UserRepository userRepository;
    private EmailService emailService;
    
    public UserService(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    public User registerUser(String name, String email) {
        // ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ
        if (userRepository.existsByEmail(email)) {
            throw new DuplicateEmailException("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค");
        }
        
        // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
        User user = new User(name, email);
        User savedUser = userRepository.save(user);
        
        // ํ™˜์˜ ์ด๋ฉ”์ผ ๋ฐœ์†ก
        emailService.sendWelcomeEmail(email);
        
        return savedUser;
    }
    
    public User getUserById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"));
    }
}

์ด์ œ ์ด ์„œ๋น„์Šค๋ฅผ Mockito๋กœ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž:

import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    @DisplayName("์ƒˆ ์‚ฌ์šฉ์ž ๋“ฑ๋ก ์„ฑ๊ณต")
    void testRegisterUser_Success() {
        // Given
        String name = "๊น€์ฒ ์ˆ˜";
        String email = "kim@example.com";
        User expectedUser = new User(1L, name, email);
        
        when(userRepository.existsByEmail(email)).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(expectedUser);
        
        // When
        User result = userService.registerUser(name, email);
        
        // Then
        assertNotNull(result);
        assertEquals(name, result.getName());
        assertEquals(email, result.getEmail());
        
        // ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ ๊ฒ€์ฆ
        verify(userRepository).existsByEmail(email);
        verify(userRepository).save(any(User.class));
        verify(emailService).sendWelcomeEmail(email);
    }
    
    @Test
    @DisplayName("์ค‘๋ณต ์ด๋ฉ”์ผ๋กœ ๋“ฑ๋ก ์‹œ๋„ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ")
    void testRegisterUser_DuplicateEmail() {
        // Given
        String email = "duplicate@example.com";
        when(userRepository.existsByEmail(email)).thenReturn(true);
        
        // When & Then
        assertThrows(DuplicateEmailException.class, () -> {
            userService.registerUser("ํ…Œ์ŠคํŠธ", email);
        });
        
        // save์™€ ์ด๋ฉ”์ผ ๋ฐœ์†ก์€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•„์•ผ ํ•จ
        verify(userRepository, never()).save(any(User.class));
        verify(emailService, never()).sendWelcomeEmail(anyString());
    }
    
    @Test
    @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ์กฐํšŒ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ")
    void testGetUserById_NotFound() {
        // Given
        Long userId = 999L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());
        
        // When & Then
        assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(userId);
        });
    }
}

๋ณด๋‹ค์‹œํ”ผ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋‚˜ ์ด๋ฉ”์ผ ์„œ๋ฒ„ ์—†์ด๋„ ์™„๋ฒฝํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด! @InjectMocks ์–ด๋…ธํ…Œ์ด์…˜์€ Mock ๊ฐ์ฒด๋“ค์„ ์ž๋™์œผ๋กœ ์ฃผ์ž…ํ•ด์ค˜์„œ ์ •๋ง ํŽธ๋ฆฌํ•ด. ๐ŸŽ‰

๐ŸŽฏ Mockito์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค

1. ArgumentCaptor (์ธ์ž ์บก์ฒ˜)
Mock ๋ฉ”์„œ๋“œ์— ์ „๋‹ฌ๋œ ์ธ์ž๋ฅผ ์บก์ฒ˜ํ•ด์„œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ์–ด. ๋ณต์žกํ•œ ๊ฐ์ฒด๊ฐ€ ์ „๋‹ฌ๋  ๋•Œ ํŠนํžˆ ์œ ์šฉํ•ด.

@Test
void testArgumentCaptor() {
    // Given
    ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
    
    // When
    userService.registerUser("์ด์˜ํฌ", "lee@example.com");
    
    // Then
    verify(userRepository).save(userCaptor.capture());
    User capturedUser = userCaptor.getValue();
    
    assertEquals("์ด์˜ํฌ", capturedUser.getName());
    assertEquals("lee@example.com", capturedUser.getEmail());
}

2. Answer (์ปค์Šคํ…€ ๋™์ž‘)
๋” ๋ณต์žกํ•œ ๋™์ž‘์ด ํ•„์š”ํ•  ๋•Œ๋Š” Answer๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด.

when(mockRepository.save(any(User.class))).thenAnswer(invocation -> {
    User user = invocation.getArgument(0);
    user.setId(100L); // ID ์ž๋™ ์ƒ์„ฑ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
    user.setCreatedAt(LocalDateTime.now());
    return user;
});

3. Spy (๋ถ€๋ถ„ ๋ชจํ‚น)
์‹ค์ œ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋˜ ์ผ๋ถ€ ๋ฉ”์„œ๋“œ๋งŒ ๋ชจํ‚นํ•˜๊ณ  ์‹ถ์„ ๋•Œ Spy๋ฅผ ์‚ฌ์šฉํ•ด.

@Spy
private UserService userService;

@Test
void testWithSpy() {
    // ์‹ค์ œ ๋ฉ”์„œ๋“œ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜๋˜, ํŠน์ • ๋ฉ”์„œ๋“œ๋งŒ ๋ชจํ‚น
    doReturn(new User(1L, "ํ…Œ์ŠคํŠธ")).when(userService).getUserById(1L);
    
    // ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋“ค์€ ์‹ค์ œ๋กœ ์‹คํ–‰๋จ
    User user = userService.getUserById(1L);
    assertNotNull(user);
}

4. BDD ์Šคํƒ€์ผ (given-when-then)
Mockito๋Š” BDD(Behavior-Driven Development) ์Šคํƒ€์ผ๋„ ์ง€์›ํ•ด. ์ฝ”๋“œ๊ฐ€ ๋” ์ฝ๊ธฐ ์‰ฌ์›Œ์ ธ.

import static org.mockito.BDDMockito.*;

@Test
void testBDDStyle() {
    // Given
    given(userRepository.findById(1L))
        .willReturn(Optional.of(new User(1L, "ํ…Œ์ŠคํŠธ")));
    
    // When
    User user = userService.getUserById(1L);
    
    // Then
    then(userRepository).should().findById(1L);
    assertNotNull(user);
}

โš ๏ธ Mockito ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ

1. ๊ณผ๋„ํ•œ ๋ชจํ‚น ์ฃผ์˜
๋ชจ๋“  ๊ฑธ ๋ชจํ‚นํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹ค์ œ ๋™์ž‘๊ณผ ๋™๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์–ด. ์ •๋ง ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ๋ชจํ‚นํ•˜์ž.

2. final ํด๋ž˜์Šค/๋ฉ”์„œ๋“œ ๋ชจํ‚น
๊ธฐ๋ณธ์ ์œผ๋กœ Mockito๋Š” final ํด๋ž˜์Šค๋‚˜ ๋ฉ”์„œ๋“œ๋ฅผ ๋ชจํ‚นํ•  ์ˆ˜ ์—†์–ด. mockito-inline ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•˜๋ฉด ๊ฐ€๋Šฅํ•˜๊ธด ํ•ด.

3. static ๋ฉ”์„œ๋“œ ๋ชจํ‚น
Mockito 3.4.0๋ถ€ํ„ฐ static ๋ฉ”์„œ๋“œ ๋ชจํ‚น์ด ๊ฐ€๋Šฅํ•ด์กŒ์ง€๋งŒ, ๊ฐ€๋Šฅํ•˜๋ฉด ํ”ผํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์„ค๊ณ„๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๊ฒŒ ๋” ๋‚˜์•„.

4. ๊ฒ€์ฆ ์ˆœ์„œ
verify๋Š” ํ…Œ์ŠคํŠธ์˜ ๋งˆ์ง€๋ง‰์— ํ•˜๋Š” ๊ฒŒ ์ข‹์•„. ์ค‘๊ฐ„์— ํ•˜๋ฉด ์ฝ”๋“œ ํ๋ฆ„์ด ๋ณต์žกํ•ด์ ธ.
Mockito ๋™์ž‘ ์›๋ฆฌ ์‹ค์ œ ๊ฐ์ฒด UserRepository ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ์‹คํ–‰ ๋А๋ฆฐ ์‘๋‹ต ์‹œ๊ฐ„ Mock ๊ฐ์ฒด Mock UserRepository ๊ฐ€์งœ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ ์ฆ‰์‹œ ์‘๋‹ต ์ œ์–ด ๊ฐ€๋Šฅํ•œ ๋™์ž‘ ๋Œ€์ฒด Mockito์˜ ์žฅ์  ๋น ๋ฅธ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์™ธ๋ถ€ ์˜์กด์„ฑ ์ œ๊ฑฐ ์˜ˆ์™ธ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ ์šฉ์ด ๊ฒฉ๋ฆฌ๋œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋™์ž‘ ๊ฒ€์ฆ ๊ฐ€๋Šฅ ์œ ์—ฐํ•œ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค

๐Ÿ”ง ์‹ค์ „ ํ”„๋กœ์ ํŠธ์— TDD ์ ์šฉํ•˜๊ธฐ

์ด๋ก ์€ ์ถฉ๋ถ„ํžˆ ๋ฐฐ์› ์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•˜๋Š”์ง€ ์•Œ์•„๋ณด์ž! ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด TDD๋ฅผ ๋ฐฐ์šฐ๊ณ ๋„ ์‹ค์ „์—์„œ ๋ง‰๋ง‰ํ•ดํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„. ๊ทธ๋ž˜์„œ ๋‹จ๊ณ„๋ณ„๋กœ ์ž์„ธํžˆ ์„ค๋ช…ํ•ด์ค„๊ฒŒ. ๐Ÿ˜Š

๐ŸŽฌ ํ”„๋กœ์ ํŠธ ์‹œ์ž‘ํ•˜๊ธฐ

๊ฐ„๋‹จํ•œ ๋„์„œ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์„ TDD๋กœ ๋งŒ๋“ค์–ด๋ณด์ž. ์ฑ…์„ ์ถ”๊ฐ€ํ•˜๊ณ , ์กฐํšŒํ•˜๊ณ , ๋Œ€์—ฌํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๊ฑฐ์•ผ.

Step 1: ์š”๊ตฌ์‚ฌํ•ญ ์ •๋ฆฌ

๋จผ์ € ๋ฌด์—‡์„ ๋งŒ๋“ค์ง€ ๋ช…ํ™•ํžˆ ํ•ด์•ผ ํ•ด. ์š”๊ตฌ์‚ฌํ•ญ์„ ์ž‘์€ ๋‹จ์œ„๋กœ ์ชผ๊ฐœ๋ณด์ž:

๐Ÿ“Œ ์ฑ… ์ถ”๊ฐ€ ๊ธฐ๋Šฅ
- ์ œ๋ชฉ, ์ €์ž, ISBN์„ ์ž…๋ ฅ๋ฐ›์•„ ์ฑ…์„ ์ถ”๊ฐ€ํ•œ๋‹ค
- ๊ฐ™์€ ISBN์˜ ์ฑ…์€ ์ค‘๋ณต ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†๋‹ค
- ํ•„์ˆ˜ ์ •๋ณด๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค

๐Ÿ“Œ ์ฑ… ์กฐํšŒ ๊ธฐ๋Šฅ
- ISBN์œผ๋กœ ์ฑ…์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค
- ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ฑ…์„ ์กฐํšŒํ•˜๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค
- ๋ชจ๋“  ์ฑ… ๋ชฉ๋ก์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿ“Œ ์ฑ… ๋Œ€์—ฌ ๊ธฐ๋Šฅ
- ์‚ฌ์šฉ์ž๊ฐ€ ์ฑ…์„ ๋Œ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค
- ์ด๋ฏธ ๋Œ€์—ฌ ์ค‘์ธ ์ฑ…์€ ๋Œ€์—ฌํ•  ์ˆ˜ ์—†๋‹ค
- ๋Œ€์—ฌ ๊ธฐ๋ก์„ ๋‚จ๊ธด๋‹ค

Step 2: ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ (Red)

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์ž. ์ฑ…์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ธฐ๋Šฅ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ๋จผ์ € ์ž‘์„ฑํ•ด.

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class BookServiceTest {
    
    private BookService bookService;
    private BookRepository bookRepository;
    
    @BeforeEach
    void setUp() {
        bookRepository = new InMemoryBookRepository();
        bookService = new BookService(bookRepository);
    }
    
    @Test
    @DisplayName("์ฑ…์„ ์„ฑ๊ณต์ ์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค")
    void testAddBook_Success() {
        // Given
        String title = "ํด๋ฆฐ ์ฝ”๋“œ";
        String author = "๋กœ๋ฒ„ํŠธ C. ๋งˆํ‹ด";
        String isbn = "978-8966260959";
        
        // When
        Book book = bookService.addBook(title, author, isbn);
        
        // Then
        assertNotNull(book);
        assertEquals(title, book.getTitle());
        assertEquals(author, book.getAuthor());
        assertEquals(isbn, book.getIsbn());
        assertFalse(book.isRented());
    }
}

์ด ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋‹น์—ฐํžˆ ์‹คํŒจํ•ด. BookService๋„, Book ํด๋ž˜์Šค๋„ ์•„์ง ์—†์œผ๋‹ˆ๊นŒ! ์ด๊ฒŒ ๋ฐ”๋กœ Red ๋‹จ๊ณ„์•ผ. ๐Ÿ”ด

Step 3: ์ตœ์†Œํ•œ์˜ ๊ตฌํ˜„ (Green)

์ด์ œ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ์ตœ์†Œํ•œ์˜ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ž.

// Book ํด๋ž˜์Šค
public class Book {
    private String title;
    private String author;
    private String isbn;
    private boolean rented;
    
    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.rented = false;
    }
    
    // Getters
    public String getTitle() { return title; }
    public String getAuthor() { return author; }
    public String getIsbn() { return isbn; }
    public boolean isRented() { return rented; }
}

// BookRepository ์ธํ„ฐํŽ˜์ด์Šค
public interface BookRepository {
    Book save(Book book);
    Optional<Book> findByIsbn(String isbn);
    List<Book> findAll();
    boolean existsByIsbn(String isbn);
}

// BookService ํด๋ž˜์Šค
public class BookService {
    private BookRepository bookRepository;
    
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    public Book addBook(String title, String author, String isbn) {
        Book book = new Book(title, author, isbn);
        return bookRepository.save(book);
    }
}

ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ†ต๊ณผํ•  ๊ฑฐ์•ผ! ๐ŸŸข ํ•˜์ง€๋งŒ ์•„์ง ๊ฒ€์ฆ ๋กœ์ง์ด ์—†์–ด. ๋‹ค์Œ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์ž.

Step 4: ์˜ˆ์™ธ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ
@Test
@DisplayName("์ค‘๋ณต๋œ ISBN์œผ๋กœ ์ฑ…์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค")
void testAddBook_DuplicateIsbn() {
    // Given
    String isbn = "978-8966260959";
    bookService.addBook("ํด๋ฆฐ ์ฝ”๋“œ", "๋กœ๋ฒ„ํŠธ C. ๋งˆํ‹ด", isbn);
    
    // When & Then
    assertThrows(DuplicateIsbnException.class, () -> {
        bookService.addBook("๋‹ค๋ฅธ ์ฑ…", "๋‹ค๋ฅธ ์ €์ž", isbn);
    });
}

@Test
@DisplayName("ํ•„์ˆ˜ ์ •๋ณด๊ฐ€ ๋ˆ„๋ฝ๋˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค")
void testAddBook_MissingRequiredFields() {
    assertThrows(IllegalArgumentException.class, () -> {
        bookService.addBook(null, "์ €์ž", "123");
    });
    
    assertThrows(IllegalArgumentException.class, () -> {
        bookService.addBook("์ œ๋ชฉ", null, "123");
    });
    
    assertThrows(IllegalArgumentException.class, () -> {
        bookService.addBook("์ œ๋ชฉ", "์ €์ž", null);
    });
}

์ด ํ…Œ์ŠคํŠธ๋“ค์„ ํ†ต๊ณผ์‹œํ‚ค๊ธฐ ์œ„ํ•ด BookService๋ฅผ ๊ฐœ์„ ํ•˜์ž:

public class BookService {
    private BookRepository bookRepository;
    
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
    
    public Book addBook(String title, String author, String isbn) {
        // ์ž…๋ ฅ ๊ฒ€์ฆ
        validateBookInfo(title, author, isbn);
        
        // ์ค‘๋ณต ์ฒดํฌ
        if (bookRepository.existsByIsbn(isbn)) {
            throw new DuplicateIsbnException("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ISBN์ž…๋‹ˆ๋‹ค: " + isbn);
        }
        
        Book book = new Book(title, author, isbn);
        return bookRepository.save(book);
    }
    
    private void validateBookInfo(String title, String author, String isbn) {
        if (title == null || title.trim().isEmpty()) {
            throw new IllegalArgumentException("์ฑ… ์ œ๋ชฉ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค");
        }
        if (author == null || author.trim().isEmpty()) {
            throw new IllegalArgumentException("์ €์ž๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค");
        }
        if (isbn == null || isbn.trim().isEmpty()) {
            throw new IllegalArgumentException("ISBN์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค");
        }
    }
}
Step 5: Mockito๋ฅผ ํ™œ์šฉํ•œ ํ…Œ์ŠคํŠธ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•  ๊ฑฐ์•ผ. Mockito๋กœ Repository๋ฅผ ๋ชจํ‚นํ•ด์„œ ํ…Œ์ŠคํŠธํ•˜์ž.

import org.mockito.*;
import static org.mockito.Mockito.*;

class BookServiceWithMockTest {
    
    @Mock
    private BookRepository bookRepository;
    
    @InjectMocks
    private BookService bookService;
    
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    @DisplayName("์ฑ… ์ถ”๊ฐ€ ์‹œ Repository์˜ save๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค")
    void testAddBook_CallsRepositorySave() {
        // Given
        String title = "์ดํŽ™ํ‹ฐ๋ธŒ ์ž๋ฐ”";
        String author = "์กฐ์Šˆ์•„ ๋ธ”๋กœํฌ";
        String isbn = "978-8966262281";
        
        when(bookRepository.existsByIsbn(isbn)).thenReturn(false);
        when(bookRepository.save(any(Book.class)))
            .thenAnswer(invocation -> invocation.getArgument(0));
        
        // When
        Book book = bookService.addBook(title, author, isbn);
        
        // Then
        verify(bookRepository).existsByIsbn(isbn);
        verify(bookRepository).save(any(Book.class));
        assertNotNull(book);
    }
    
    @Test
    @DisplayName("๋Œ€์—ฌ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ")
    void testRentBook() {
        // Given
        String isbn = "978-8966260959";
        String userId = "user123";
        Book book = new Book("ํด๋ฆฐ ์ฝ”๋“œ", "๋กœ๋ฒ„ํŠธ C. ๋งˆํ‹ด", isbn);
        
        when(bookRepository.findByIsbn(isbn))
            .thenReturn(Optional.of(book));
        
        // When
        bookService.rentBook(isbn, userId);
        
        // Then
        assertTrue(book.isRented());
        verify(bookRepository).save(book);
    }
}

๐Ÿ—๏ธ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ vs ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

TDD๋ฅผ ํ•˜๋‹ค ๋ณด๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ์–ด.

๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test)
- ํ•˜๋‚˜์˜ ํด๋ž˜์Šค๋‚˜ ๋ฉ”์„œ๋“œ๋งŒ ํ…Œ์ŠคํŠธ
- ์™ธ๋ถ€ ์˜์กด์„ฑ์€ ๋ชจ๋‘ ๋ชจํ‚น
- ๋น ๋ฅด๊ฒŒ ์‹คํ–‰๋จ
- ๊ฐœ๋ฐœ ์ค‘์— ์ž์ฃผ ์‹คํ–‰

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test)
- ์—ฌ๋Ÿฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•จ๊ป˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ
- ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์™ธ๋ถ€ API ์‚ฌ์šฉ
- ์‹คํ–‰ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆผ
- ๋ฐฐํฌ ์ „์— ์‹คํ–‰

Spring Boot๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด:

@SpringBootTest
@AutoConfigureTestDatabase
class BookServiceIntegrationTest {
    
    @Autowired
    private BookService bookService;
    
    @Autowired
    private BookRepository bookRepository;
    
    @BeforeEach
    void setUp() {
        bookRepository.deleteAll();
    }
    
    @Test
    @DisplayName("์ฑ… ์ถ”๊ฐ€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ")
    void testAddBook_Integration() {
        // Given
        String title = "ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜";
        String author = "๋กœ๋ฒ„ํŠธ C. ๋งˆํ‹ด";
        String isbn = "978-8966262472";
        
        // When
        Book book = bookService.addBook(title, author, isbn);
        
        // Then
        assertNotNull(book);
        
        // ์‹ค์ œ๋กœ DB์— ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
        Optional<Book> found = bookRepository.findByIsbn(isbn);
        assertTrue(found.isPresent());
        assertEquals(title, found.get().getTitle());
    }
}

๐Ÿ’Ž TDD ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

TDD๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ช‡ ๊ฐ€์ง€ ์›์น™์„ ์ง€ํ‚ค๋Š” ๊ฒŒ ์ข‹์•„. ์‹ค์ „ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ •๋ฆฌํ•œ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋ฅผ ๊ณต์œ ํ• ๊ฒŒ! ๐ŸŒŸ

1๏ธโƒฃ ํ…Œ์ŠคํŠธ๋Š” ์ž‘๊ณ  ๋น ๋ฅด๊ฒŒ

FIRST ์›์น™

Fast: ํ…Œ์ŠคํŠธ๋Š” ๋นจ๋ผ์•ผ ํ•ด. ๋А๋ฆฐ ํ…Œ์ŠคํŠธ๋Š” ์ž์ฃผ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ฒŒ ๋ผ.
Independent: ํ…Œ์ŠคํŠธ๋Š” ์„œ๋กœ ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•ด. ์ˆœ์„œ์— ์˜์กดํ•˜๋ฉด ์•ˆ ๋ผ.
Repeatable: ์–ด๋–ค ํ™˜๊ฒฝ์—์„œ๋„ ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•ด์•ผ ํ•ด.
Self-validating: ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” ๋ช…ํ™•ํ•ด์•ผ ํ•ด. ์„ฑ๊ณต ์•„๋‹ˆ๋ฉด ์‹คํŒจ.
Timely: ํ…Œ์ŠคํŠธ๋Š” ์ ์‹œ์— ์ž‘์„ฑํ•ด์•ผ ํ•ด. ๊ตฌํ˜„ ์ „์—!

2๏ธโƒฃ ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ์ด๋ฆ„

ํ…Œ์ŠคํŠธ ์ด๋ฆ„๋งŒ ๋ด๋„ ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ์–ด์•ผ ํ•ด. ์ข‹์€ ์˜ˆ์™€ ๋‚˜์œ ์˜ˆ๋ฅผ ๋น„๊ตํ•ด๋ณผ๊นŒ?

// โŒ ๋‚˜์œ ์˜ˆ
@Test
void test1() { }

@Test
void testAdd() { }

// โœ… ์ข‹์€ ์˜ˆ
@Test
@DisplayName("์–‘์ˆ˜ ๋‘ ๊ฐœ๋ฅผ ๋”ํ•˜๋ฉด ์˜ฌ๋ฐ”๋ฅธ ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค")
void shouldReturnCorrectSum_WhenAddingTwoPositiveNumbers() { }

@Test
@DisplayName("0์œผ๋กœ ๋‚˜๋ˆ„๋ฉด ArithmeticException์ด ๋ฐœ์ƒํ•œ๋‹ค")
void shouldThrowArithmeticException_WhenDividingByZero() { }

3๏ธโƒฃ AAA ํŒจํ„ด (Arrange-Act-Assert)

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์„ธ ๋ถ€๋ถ„์œผ๋กœ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๋ฉด ๊ฐ€๋…์„ฑ์ด ์ข‹์•„์ ธ.

@Test
void testUserRegistration() {
    // Arrange (์ค€๋น„): ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ์™€ ์ƒํƒœ ์„ค์ •
    String email = "test@example.com";
    String password = "password123";
    UserService userService = new UserService(mockRepository);
    
    // Act (์‹คํ–‰): ํ…Œ์ŠคํŠธํ•  ๋™์ž‘ ์ˆ˜ํ–‰
    User user = userService.register(email, password);
    
    // Assert (๊ฒ€์ฆ): ๊ฒฐ๊ณผ ํ™•์ธ
    assertNotNull(user);
    assertEquals(email, user.getEmail());
    assertTrue(user.isActive());
}

4๏ธโƒฃ ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜์˜ ๊ฐœ๋…๋งŒ

ํ•œ ํ…Œ์ŠคํŠธ์—์„œ ๋„ˆ๋ฌด ๋งŽ์€ ๊ฑธ ๊ฒ€์ฆํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ ์›์ธ์„ ์ฐพ๊ธฐ ์–ด๋ ค์›Œ์ ธ.

// โŒ ๋‚˜์œ ์˜ˆ: ์—ฌ๋Ÿฌ ๊ฐœ๋…์„ ํ•œ ๋ฒˆ์— ํ…Œ์ŠคํŠธ
@Test
void testUserOperations() {
    User user = userService.register("test@example.com", "pass");
    assertNotNull(user);
    
    user.setName("ํ™๊ธธ๋™");
    userService.update(user);
    assertEquals("ํ™๊ธธ๋™", user.getName());
    
    userService.delete(user.getId());
    assertFalse(userService.exists(user.getId()));
}

// โœ… ์ข‹์€ ์˜ˆ: ๊ฐ ๊ฐœ๋…์„ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธ
@Test
void testUserRegistration() {
    User user = userService.register("test@example.com", "pass");
    assertNotNull(user);
}

@Test
void testUserUpdate() {
    User user = createTestUser();
    user.setName("ํ™๊ธธ๋™");
    userService.update(user);
    assertEquals("ํ™๊ธธ๋™", user.getName());
}

@Test
void testUserDeletion() {
    User user = createTestUser();
    userService.delete(user.getId());
    assertFalse(userService.exists(user.getId()));
}

5๏ธโƒฃ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•๋“ค์ด ์žˆ์–ด:

// Test Fixture ํŒจํ„ด
public class TestFixtures {
    public static User createTestUser() {
        return new User("test@example.com", "password", "ํ…Œ์ŠคํŠธ์œ ์ €");
    }
    
    public static User createTestUser(String email) {
        return new User(email, "password", "ํ…Œ์ŠคํŠธ์œ ์ €");
    }
    
    public static Book createTestBook() {
        return new Book("ํ…Œ์ŠคํŠธ ์ฑ…", "ํ…Œ์ŠคํŠธ ์ €์ž", "123-456");
    }
}

// Builder ํŒจํ„ด
public class UserBuilder {
    private String email = "test@example.com";
    private String password = "password";
    private String name = "ํ…Œ์ŠคํŠธ";
    private boolean active = true;
    
    public UserBuilder email(String email) {
        this.email = email;
        return this;
    }
    
    public UserBuilder password(String password) {
        this.password = password;
        return this;
    }
    
    public UserBuilder inactive() {
        this.active = false;
        return this;
    }
    
    public User build() {
        User user = new User(email, password, name);
        user.setActive(active);
        return user;
    }
}

// ์‚ฌ์šฉ ์˜ˆ
@Test
void testInactiveUser() {
    User user = new UserBuilder()
        .email("inactive@example.com")
        .inactive()
        .build();
    
    assertFalse(user.isActive());
}

6๏ธโƒฃ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€์˜ ํ•จ์ •

๋งŽ์€ ํŒ€์ด "ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ 80% ์ด์ƒ"๊ฐ™์€ ๋ชฉํ‘œ๋ฅผ ์„ธ์›Œ. ํ•˜์ง€๋งŒ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ๋†’๋‹ค๊ณ  ํ•ด์„œ ์ข‹์€ ํ…Œ์ŠคํŠธ๋Š” ์•„๋‹ˆ์•ผ! ๐Ÿ˜…

์ค‘์š”ํ•œ ๊ฑด ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ์•ผ:
โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ œ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๋Š”๊ฐ€?
โœ… ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ๋‹ค๋ฃจ๋Š”๊ฐ€?
โœ… ์˜ˆ์™ธ ์ƒํ™ฉ์„ ํ…Œ์ŠคํŠธํ•˜๋Š”๊ฐ€?
โœ… ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋ฉด ์‹ค์ œ ๋ฌธ์ œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์žˆ๋Š”๊ฐ€?

๋‹จ์ˆœํžˆ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•œ ํ…Œ์ŠคํŠธ๋Š” ์˜คํžˆ๋ ค ์œ ์ง€๋ณด์ˆ˜ ๋ถ€๋‹ด๋งŒ ๋Š˜๋ ค.

7๏ธโƒฃ ๋ฆฌํŒฉํ† ๋ง๊ณผ ํ…Œ์ŠคํŠธ

TDD์˜ ๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„์ธ ๋ฆฌํŒฉํ† ๋ง์€ ์ •๋ง ์ค‘์š”ํ•ด. ํ…Œ์ŠคํŠธ๊ฐ€ ์žˆ์œผ๋‹ˆ๊นŒ ์•ˆ์‹ฌํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์–ด!

// ๋ฆฌํŒฉํ† ๋ง ์ „
public class OrderService {
    public double calculateTotal(Order order) {
        double total = 0;
        for (OrderItem item : order.getItems()) {
            total += item.getPrice() * item.getQuantity();
        }
        if (order.getCustomer().isPremium()) {
            total = total * 0.9; // 10% ํ• ์ธ
        }
        if (total > 100000) {
            total = total * 0.95; // 5% ์ถ”๊ฐ€ ํ• ์ธ
        }
        return total;
    }
}

// ๋ฆฌํŒฉํ† ๋ง ํ›„
public class OrderService {
    private static final double PREMIUM_DISCOUNT = 0.9;
    private static final double BULK_DISCOUNT = 0.95;
    private static final double BULK_THRESHOLD = 100000;
    
    public double calculateTotal(Order order) {
        double subtotal = calculateSubtotal(order);
        double afterPremiumDiscount = applyPremiumDiscount(subtotal, order);
        return applyBulkDiscount(afterPremiumDiscount);
    }
    
    private double calculateSubtotal(Order order) {
        return order.getItems().stream()
            .mapToDouble(item -> item.getPrice() * item.getQuantity())
            .sum();
    }
    
    private double applyPremiumDiscount(double amount, Order order) {
        return order.getCustomer().isPremium() 
            ? amount * PREMIUM_DISCOUNT 
            : amount;
    }
    
    private double applyBulkDiscount(double amount) {
        return amount > BULK_THRESHOLD 
            ? amount * BULK_DISCOUNT 
            : amount;
    }
}

๋ฆฌํŒฉํ† ๋ง ํ›„์—๋„ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋ฉด, ์ฝ”๋“œ๊ฐ€ ์—ฌ์ „ํžˆ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•œ๋‹ค๋Š” ํ™•์‹ ์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์–ด! ๐ŸŽ‰

๐Ÿšง TDD์˜ ํ˜„์‹ค์ ์ธ ์–ด๋ ค์›€๊ณผ ํ•ด๊ฒฐ์ฑ…

์†”์งํžˆ ๋งํ• ๊ฒŒ. TDD๋Š” ์ด์ƒ์ ์œผ๋กœ๋Š” ์™„๋ฒฝํ•˜์ง€๋งŒ, ํ˜„์‹ค์—์„œ๋Š” ์—ฌ๋Ÿฌ ์–ด๋ ค์›€์ด ์žˆ์–ด. ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๋ฌธ์ œ๋“ค์„ ์ธ์‹ํ•˜๊ณ  ๋Œ€์ฒ˜ํ•˜๋ฉด ์ถฉ๋ถ„ํžˆ ๊ทน๋ณตํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ’ช

๐Ÿ˜ฐ ์–ด๋ ค์›€ 1: ์‹œ๊ฐ„ ์••๋ฐ•

๋ฌธ์ œ: "ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์งค ์‹œ๊ฐ„์ด ์–ด๋”” ์žˆ์–ด์š”? ๊ธฐ๋Šฅ ๊ตฌํ˜„๋„ ๋ฐ”์œ๋ฐ!"

ํ•ด๊ฒฐ์ฑ…:
- ์ฒ˜์Œ์—๋Š” ์‹œ๊ฐ„์ด ๋” ๊ฑธ๋ฆฌ๋Š” ๊ฒŒ ๋งž์•„. ํ•˜์ง€๋งŒ ์žฅ๊ธฐ์ ์œผ๋กœ๋Š” ๋””๋ฒ„๊น… ์‹œ๊ฐ„์ด ์ค„์–ด๋“ค์–ด์„œ ์˜คํžˆ๋ ค ๋นจ๋ผ์ ธ.
- ๋ชจ๋“  ์ฝ”๋“œ์— TDD๋ฅผ ์ ์šฉํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ค‘์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด.
- ํŒ€ ์ „์ฒด๊ฐ€ TDD์— ์ต์ˆ™ํ•ด์ง€๋ฉด ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ์ ์  ๋นจ๋ผ์ ธ.
- ๋งค๋‹ˆ์ €๋‚˜ ํŒ€์žฅ์—๊ฒŒ TDD์˜ ์žฅ๊ธฐ์  ์ด์ ์„ ์„ค๋ช…ํ•˜๊ณ  ์ดํ•ด๋ฅผ ๊ตฌํ•ด.

๐Ÿ˜ต ์–ด๋ ค์›€ 2: ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ

๋ฌธ์ œ: "์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ์ฝ”๋“œ๊ฐ€ ์‚ฐ๋”๋ฏธ์ธ๋ฐ ์–ด๋–ป๊ฒŒ TDD๋ฅผ ํ•ด์š”?"

ํ•ด๊ฒฐ์ฑ…:
- ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์€ TDD๋กœ ๊ฐœ๋ฐœํ•ด.
- ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ•  ๋•Œ, ๋จผ์ € ๋ฒ„๊ทธ๋ฅผ ์žฌํ˜„ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด.
- ๋ฆฌํŒฉํ† ๋งํ•  ๋•Œ ์ ์ง„์ ์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ด.
- "Characterization Tests"๋ฅผ ์ž‘์„ฑํ•ด์„œ ๊ธฐ์กด ์ฝ”๋“œ์˜ ๋™์ž‘์„ ๋ฌธ์„œํ™”ํ•ด.

// ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ์˜ ๋™์ž‘์„ ํ…Œ์ŠคํŠธ๋กœ ๋ฌธ์„œํ™”
@Test
void characterizeExistingBehavior() {
    // ๊ธฐ์กด ์ฝ”๋“œ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ๋กœ ๊ธฐ๋ก
    LegacyService service = new LegacyService();
    String result = service.doSomething("input");
    
    // ํ˜„์žฌ ๋™์ž‘์„ assert๋กœ ๊ณ ์ •
    assertEquals("expected output", result);
    // ์ด์ œ ๋ฆฌํŒฉํ† ๋งํ•ด๋„ ๋™์ž‘์ด ๋ณ€ํ•˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ ๊ฐ€๋Šฅ!
}

๐Ÿคฏ ์–ด๋ ค์›€ 3: ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ

๋ฌธ์ œ: "์ด ์ฝ”๋“œ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ด์š”!"

ํ•ด๊ฒฐ์ฑ…:
๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ "ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ"๋Š” "์„ค๊ณ„๊ฐ€ ์ข‹์ง€ ์•Š์€ ์ฝ”๋“œ"์•ผ. TDD๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋” ๋‚˜์€ ์„ค๊ณ„๋กœ ์ด๋Œ์–ด์ค˜.

// โŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ค์šด ์ฝ”๋“œ
public class UserService {
    public void registerUser(String email, String password) {
        // ์ง์ ‘ DB์— ์—ฐ๊ฒฐ
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");
        // ์ง์ ‘ ์ด๋ฉ”์ผ ๋ฐœ์†ก
        EmailSender.send(email, "Welcome!");
        // ํ˜„์žฌ ์‹œ๊ฐ„ ์ง์ ‘ ์‚ฌ์šฉ
        user.setCreatedAt(new Date());
    }
}

// โœ… ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ
public class UserService {
    private UserRepository repository;
    private EmailService emailService;
    private Clock clock;
    
    public UserService(UserRepository repository, 
                      EmailService emailService,
                      Clock clock) {
        this.repository = repository;
        this.emailService = emailService;
        this.clock = clock;
    }
    
    public void registerUser(String email, String password) {
        User user = new User(email, password);
        user.setCreatedAt(clock.now());
        repository.save(user);
        emailService.sendWelcomeEmail(email);
    }
}

๐Ÿ˜ค ์–ด๋ ค์›€ 4: ํŒ€์›๋“ค์˜ ์ €ํ•ญ

๋ฌธ์ œ: "ํŒ€์›๋“ค์ด TDD๋ฅผ ํ•˜๊ธฐ ์‹ซ์–ดํ•ด์š”."

ํ•ด๊ฒฐ์ฑ…:
- ๊ฐ•์š”ํ•˜์ง€ ๋ง๊ณ  ๋จผ์ € ๋ณธ์ธ์ด ์‹ค์ฒœํ•ด์„œ ํšจ๊ณผ๋ฅผ ๋ณด์—ฌ์ค˜.
- ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ์œผ๋กœ ํ•จ๊ป˜ TDD๋ฅผ ๊ฒฝํ—˜ํ•ด๋ด.
- ์ž‘์€ ์„ฑ๊ณต ์‚ฌ๋ก€๋ฅผ ๊ณต์œ ํ•ด.
- TDD ์Šคํ„ฐ๋””๋‚˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ๋ฅผ ํ†ตํ•ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ•™์Šต ๊ธฐํšŒ๋ฅผ ๋งŒ๋“ค์–ด.
- ์™„๋ฒฝํ•œ TDD๊ฐ€ ์•„๋‹ˆ์–ด๋„ ๊ดœ์ฐฎ์•„. ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋ฉด ๋ผ.

๐ŸŽฏ ์–ด๋ ค์›€ 5: ๋ฌด์—‡์„ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ• ์ง€ ๋ชจ๋ฅด๊ฒ ์–ด์š”

๋ฌธ์ œ: "ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ  ์‹ถ์€๋ฐ ์–ด๋””์„œ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์•ผ ํ• ์ง€ ๋ง‰๋ง‰ํ•ด์š”."

ํ•ด๊ฒฐ์ฑ…:
์ด๋Ÿฐ ์ˆœ์„œ๋กœ ์ƒ๊ฐํ•ด๋ด:

1. Happy Path (์ •์ƒ ๊ฒฝ๋กœ)
๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์„ฑ๊ณต ์‹œ๋‚˜๋ฆฌ์˜ค๋ถ€ํ„ฐ ํ…Œ์ŠคํŠธํ•ด.

2. Edge Cases (๊ฒฝ๊ณ„ ์กฐ๊ฑด)
0, ์Œ์ˆ˜, ์ตœ๋Œ“๊ฐ’, ์ตœ์†Ÿ๊ฐ’, ๋นˆ ๋ฌธ์ž์—ด ๋“ฑ์„ ํ…Œ์ŠคํŠธํ•ด.

3. Error Cases (์˜ค๋ฅ˜ ์ƒํ™ฉ)
null, ์ž˜๋ชป๋œ ์ž…๋ ฅ, ์˜ˆ์™ธ ์ƒํ™ฉ์„ ํ…Œ์ŠคํŠธํ•ด.

4. Business Rules (๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™)
๋„๋ฉ”์ธ ํŠนํ™”๋œ ๊ทœ์น™๋“ค์„ ํ…Œ์ŠคํŠธํ•ด.
// ๊ณ„์‚ฐ๊ธฐ ์˜ˆ์ œ๋กœ ๋ณด๋Š” ํ…Œ์ŠคํŠธ ์šฐ์„ ์ˆœ์œ„

// 1. Happy Path
@Test
void shouldAdd_WhenGivenTwoPositiveNumbers() {
    assertEquals(5, calculator.add(2, 3));
}

// 2. Edge Cases
@Test
void shouldHandleZero() {
    assertEquals(0, calculator.add(0, 0));
    assertEquals(5, calculator.add(5, 0));
}

@Test
void shouldHandleNegativeNumbers() {
    assertEquals(-5, calculator.add(-2, -3));
    assertEquals(1, calculator.add(-2, 3));
}

// 3. Error Cases
@Test
void shouldThrowException_WhenResultOverflows() {
    assertThrows(ArithmeticException.class, () -> {
        calculator.add(Integer.MAX_VALUE, 1);
    });
}

// 4. Business Rules
@Test
void shouldRoundResult_WhenDividing() {
    // ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™: ๋‚˜๋ˆ—์…ˆ ๊ฒฐ๊ณผ๋Š” ์†Œ์ˆ˜์  ๋‘˜์งธ์ž๋ฆฌ์—์„œ ๋ฐ˜์˜ฌ๋ฆผ
    assertEquals(3.33, calculator.divide(10, 3), 0.01);
}

๐ŸŽ“ ์‹ค๋ฌด์—์„œ ๋ฐฐ์šด TDD ํŒ๋“ค

์—ฌ๋Ÿฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฑฐ์น˜๋ฉด์„œ ๋ฐฐ์šด ์‹ค์šฉ์ ์ธ ํŒ๋“ค์„ ๊ณต์œ ํ• ๊ฒŒ. ์ฑ…์—์„œ๋Š” ์ž˜ ์•ˆ ๋‚˜์˜ค๋Š” ํ˜„์žฅ์˜ ๋…ธํ•˜์šฐ๋“ค์ด์•ผ! ๐Ÿ˜Ž

๐Ÿ’ก Tip 1: ํ…Œ์ŠคํŠธ ์ด๋ฆ„์— ํ•œ๊ธ€ ์‚ฌ์šฉํ•˜๊ธฐ

์˜์–ด๋กœ ํ…Œ์ŠคํŠธ ์ด๋ฆ„์„ ์ง“๋‹ค ๋ณด๋ฉด ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์ ธ. ํ•œ๊ธ€์„ ์‚ฌ์šฉํ•˜๋ฉด ํ›จ์”ฌ ๋ช…ํ™•ํ•ด!

// ์˜์–ด
@Test
void shouldThrowInvalidEmailExceptionWhenEmailFormatIsInvalid() { }

// ํ•œ๊ธ€
@Test
@DisplayName("์ด๋ฉ”์ผ ํ˜•์‹์ด ์ž˜๋ชป๋˜๋ฉด InvalidEmailException์ด ๋ฐœ์ƒํ•œ๋‹ค")
void ์ด๋ฉ”์ผ_ํ˜•์‹_๊ฒ€์ฆ() { }

๐Ÿ’ก Tip 2: ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ˆœ์„œ์— ์˜์กดํ•˜์ง€ ์•Š๊ธฐ

๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•ด. ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์˜ ๊ฒฐ๊ณผ์— ์˜์กดํ•˜๋ฉด ์•ˆ ๋ผ!

// โŒ ๋‚˜์œ ์˜ˆ: ํ…Œ์ŠคํŠธ ์ˆœ์„œ์— ์˜์กด
private static User savedUser;

@Test
void test1_saveUser() {
    savedUser = userService.save(new User("test@example.com"));
}

@Test
void test2_findUser() {
    User found = userService.findById(savedUser.getId());
    assertNotNull(found);
}

// โœ… ์ข‹์€ ์˜ˆ: ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋…๋ฆฝ์ 
@Test
void testSaveUser() {
    User user = userService.save(new User("test@example.com"));
    assertNotNull(user.getId());
}

@Test
void testFindUser() {
    User user = userService.save(new User("test@example.com"));
    User found = userService.findById(user.getId());
    assertNotNull(found);
}

๐Ÿ’ก Tip 3: ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋Š” ๋ช…ํ™•ํ•˜๊ฒŒ

ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋Š” ์˜๋ฏธ๊ฐ€ ๋ช…ํ™•ํ•ด์•ผ ํ•ด. "test1", "abc" ๊ฐ™์€ ์˜๋ฏธ ์—†๋Š” ๊ฐ’ ๋Œ€์‹  ์‹ค์ œ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด.

// โŒ ์˜๋ฏธ ์—†๋Š” ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ
@Test
void test() {
    User user = new User("a", "b", 1);
    // ์ด๊ฒŒ ๋ญ˜ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ฑด์ง€ ์•Œ๊ธฐ ์–ด๋ ค์›Œ
}

// โœ… ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ
@Test
void ๋ฏธ์„ฑ๋…„์ž๋Š”_์„ฑ์ธ_์ฝ˜ํ…์ธ ๋ฅผ_๋ณผ_์ˆ˜_์—†๋‹ค() {
    User minorUser = new User("๊น€์ฒ ์ˆ˜", "minor@example.com", 15);
    Content adultContent = new Content("์„ฑ์ธ ์˜ํ™”", Rating.ADULT);
    
    assertFalse(contentService.canView(minorUser, adultContent));
}

๐Ÿ’ก Tip 4: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ๋ฆฌํŒฉํ† ๋งํ•˜๊ธฐ

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ๋งŒํผ ์ค‘์š”ํ•ด. ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๊ณ  ๊ฐ€๋…์„ฑ์„ ๋†’์—ฌ์•ผ ํ•ด!

// ๊ณตํ†ต ์„ค์ •์„ ํ—ฌํผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœ
class OrderServiceTest {
    
    private Order createTestOrder() {
        Order order = new Order();
        order.addItem(new OrderItem("์ƒํ’ˆ1", 10000, 2));
        order.addItem(new OrderItem("์ƒํ’ˆ2", 20000, 1));
        return order;
    }
    
    private User createPremiumUser() {
        User user = new User("premium@example.com");
        user.setPremium(true);
        return user;
    }
    
    @Test
    void ํ”„๋ฆฌ๋ฏธ์—„_ํšŒ์›์€_10ํผ์„ผํŠธ_ํ• ์ธ() {
        Order order = createTestOrder();
        User user = createPremiumUser();
        order.setCustomer(user);
        
        double total = orderService.calculateTotal(order);
        assertEquals(36000, total); // (10000*2 + 20000) * 0.9
    }
}

๐Ÿ’ก Tip 5: ์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ

ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ–ˆ์„ ๋•Œ ์›์ธ์„ ๋น ๋ฅด๊ฒŒ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ช…ํ™•ํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ž‘์„ฑํ•ด.

// โŒ ๋ถˆ๋ช…ํ™•ํ•œ ์‹คํŒจ ๋ฉ”์‹œ์ง€
assertEquals(expected, actual);

// โœ… ๋ช…ํ™•ํ•œ ์‹คํŒจ ๋ฉ”์‹œ์ง€
assertEquals(expected, actual, 
    String.format("์ฃผ๋ฌธ ์ด์•ก์ด ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค. ์˜ˆ์ƒ: %d, ์‹ค์ œ: %d", expected, actual));

// ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•: assertJ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ
assertThat(actual)
    .as("์ฃผ๋ฌธ ์ด์•ก ๊ฒ€์ฆ")
    .isEqualTo(expected);

๐Ÿ’ก Tip 6: ํ”Œ๋ž˜ํ‚ค ํ…Œ์ŠคํŠธ(Flaky Test) ์ฃผ์˜

๊ฐ€๋” ์„ฑ๊ณตํ•˜๊ณ  ๊ฐ€๋” ์‹คํŒจํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ”Œ๋ž˜ํ‚ค ํ…Œ์ŠคํŠธ๋ผ๊ณ  ํ•ด. ์ด๋Ÿฐ ํ…Œ์ŠคํŠธ๋Š” ์ •๋ง ๊ณจ์น˜ ์•„ํŒŒ!

ํ”Œ๋ž˜ํ‚ค ํ…Œ์ŠคํŠธ์˜ ์ฃผ์š” ์›์ธ:
- ์‹œ๊ฐ„์— ์˜์กดํ•˜๋Š” ์ฝ”๋“œ
- ๋žœ๋ค ๊ฐ’ ์‚ฌ์šฉ
- ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์˜์กด
- ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ ๊ณต์œ  ์ž์› ์ ‘๊ทผ
- ํ…Œ์ŠคํŠธ ์ˆœ์„œ ์˜์กด์„ฑ

// โŒ ํ”Œ๋ž˜ํ‚ค ํ…Œ์ŠคํŠธ
@Test
void testTimeout() {
    long start = System.currentTimeMillis();
    service.doSomething();
    long end = System.currentTimeMillis();
    
    // ์‹คํ–‰ ์‹œ๊ฐ„์ด ์‹œ์Šคํ…œ ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด
    assertTrue(end - start < 1000);
}

// โœ… ๊ฐœ์„ ๋œ ํ…Œ์ŠคํŠธ
@Test
@Timeout(value = 1, unit = TimeUnit.SECONDS)
void testTimeout() {
    service.doSomething();
    // JUnit์˜ @Timeout ์‚ฌ์šฉ
}

๐ŸŒ ์žฌ๋Šฅ๋„ท์—์„œ TDD ์ „๋ฌธ๊ฐ€ ์ฐพ๊ธฐ

TDD์™€ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ˜ผ์ž ๊ณต๋ถ€ํ•˜๋‹ค ๋ณด๋ฉด ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์–ด. ๊ทธ๋Ÿด ๋•Œ๋Š” ์ „๋ฌธ๊ฐ€์˜ ๋„์›€์„ ๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ! ๐ŸŽ“

์žฌ๋Šฅ๋„ท์—์„œ๋Š” ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์ „๋ฌธ๊ฐ€๋“ค์ด ํ™œ๋™ํ•˜๊ณ  ์žˆ์–ด. TDD ์ฝ”์นญ, ์ฝ”๋“œ ๋ฆฌ๋ทฐ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ์ปจ์„คํŒ… ๋“ฑ ์‹ค๋ฌด์— ๋ฐ”๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์ „ ๋…ธํ•˜์šฐ๋ฅผ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์ง€. ํŠนํžˆ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— TDD๋ฅผ ๋„์ž…ํ•˜๋ ค๊ณ  ํ•  ๋•Œ, ๊ฒฝํ—˜ ์žˆ๋Š” ๋ฉ˜ํ† ์˜ ์กฐ์–ธ์ด ํฐ ๋„์›€์ด ๋  ๊ฑฐ์•ผ.

ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ์žฌ๋Šฅ๋„ท์—์„œ ๋‹น์‹ ์—๊ฒŒ ๋งž๋Š” ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฑด ์–ด๋•Œ? ๋•Œ๋กœ๋Š” ํ•œ ์‹œ๊ฐ„์˜ ๋ฉ˜ํ† ๋ง์ด ๋ฉฐ์น ๊ฐ„์˜ ์‚ฝ์งˆ์„ ์ค„์—ฌ์ค„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ˜Š

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

์™€, ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด! JUnit, TestNG, Mockito ๊ฐ™์€ ํ…Œ์ŠคํŒ… ํ”„๋ ˆ์ž„์›Œํฌ๋ถ€ํ„ฐ TDD ๋ฐฉ๋ฒ•๋ก ๊นŒ์ง€ ํ•จ๊ป˜ ์‚ดํŽด๋ดค์–ด. ๐ŸŽ‰

์ฒ˜์Œ์—๋Š” TDD๊ฐ€ ์–ด๋ ต๊ณ  ์‹œ๊ฐ„์ด ๋งŽ์ด ๊ฑธ๋ฆฌ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ ๊ณ„์† ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์ž์—ฐ์Šค๋Ÿฌ์›Œ์ง€๊ณ , ์–ด๋А์ƒˆ ํ…Œ์ŠคํŠธ ์—†์ด๋Š” ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์—†๊ฒŒ ๋  ๊ฑฐ์•ผ. ๋‚˜๋„ ๊ทธ๋žฌ๊ฑฐ๋“ ! ๐Ÿ˜„

๊ธฐ์–ตํ•ด์•ผ ํ•  ํ•ต์‹ฌ ํฌ์ธํŠธ๋“ค:

๐ŸŽฏ TDD๋Š” ๋‹จ์ˆœํžˆ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ์ด ์•„๋‹ˆ์•ผ
์„ค๊ณ„๋ฅผ ๊ฐœ์„ ํ•˜๊ณ , ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๋†’์ด๊ณ , ๊ฐœ๋ฐœ์ž์˜ ์ž์‹ ๊ฐ์„ ํ‚ค์›Œ์ฃผ๋Š” ๊ฐœ๋ฐœ ๋ฐฉ๋ฒ•๋ก ์ด์•ผ.

๐ŸŽฏ ์™„๋ฒฝ์„ ์ถ”๊ตฌํ•˜์ง€ ๋งˆ
๋ชจ๋“  ์ฝ”๋“œ์— 100% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋‹ฌ์„ฑํ•˜๋ ค๊ณ  ํ•˜์ง€ ๋งˆ. ์ค‘์š”ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋ฉด ๋ผ.

๐ŸŽฏ ๋„๊ตฌ๋Š” ์ˆ˜๋‹จ์ผ ๋ฟ์ด์•ผ
JUnit์ด๋“  TestNG๋“ , ์–ด๋–ค ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์“ฐ๋Š”์ง€๋Š” ์ค‘์š”ํ•˜์ง€ ์•Š์•„. ์ค‘์š”ํ•œ ๊ฑด ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๋†’์ด๋Š” ๊ฑฐ์•ผ.

๐ŸŽฏ ํŒ€๊ณผ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜์ž
ํ˜ผ์ž๋งŒ TDD๋ฅผ ํ•˜๋ฉด ํž˜๋“ค์–ด. ํŒ€์›๋“ค๊ณผ ํ•จ๊ป˜ ํ•™์Šตํ•˜๊ณ , ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ณ , ์„œ๋กœ ๋„์™€๊ฐ€๋ฉฐ ์„ฑ์žฅํ•˜์ž.

๐ŸŽฏ ์‹คํŒจ๋ฅผ ๋‘๋ ค์›Œํ•˜์ง€ ๋งˆ
ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•˜๋Š” ๊ฑด ๋‚˜์œ ๊ฒŒ ์•„๋‹ˆ์•ผ. ์˜คํžˆ๋ ค ๋ฌธ์ œ๋ฅผ ์ผ์ฐ ๋ฐœ๊ฒฌํ•œ ๊ฑฐ๋‹ˆ๊นŒ ์ข‹์€ ๊ฑฐ์ง€! Red-Green-Refactor ์‚ฌ์ดํด์„ ์ฆ๊ฒจ๋ด.

TDD๋Š” ๋งˆ๋ผํ†ค์ด์•ผ. ๋‹จ๊ฑฐ๋ฆฌ ๋‹ฌ๋ฆฌ๊ธฐ๊ฐ€ ์•„๋‹ˆ์ง€. ์ฒœ์ฒœํžˆ, ๊พธ์ค€ํžˆ ์—ฐ์Šตํ•˜๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋‹น์‹ ๋„ TDD ๋งˆ์Šคํ„ฐ๊ฐ€ ๋˜์–ด ์žˆ์„ ๊ฑฐ์•ผ! ๐Ÿ’ช

์ด ๊ธ€์ด ๋‹น์‹ ์˜ TDD ์—ฌ์ •์— ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋” ๊นŠ์ด ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด, ์žฌ๋Šฅ๋„ท์—์„œ ์ „๋ฌธ๊ฐ€๋“ค๊ณผ ํ•จ๊ป˜ ์„ฑ์žฅํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ถ”์ฒœํ•ด! ๐Ÿš€

์ž, ์ด์ œ ์—๋””ํ„ฐ๋ฅผ ์—ด๊ณ  ์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•ด๋ณผ๊นŒ? ํ™”์ดํŒ…! ๐ŸŽŠ

โœจ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์™€ ํ•จ๊ป˜ํ•˜๋Š” ์ฆ๊ฑฐ์šด ๊ฐœ๋ฐœ ๋˜์„ธ์š”! โœจ

Red โ†’ Green โ†’ Refactor ๐Ÿ”„

๋Œ“๊ธ€ ์ž‘์„ฑ

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

๋Œ“๊ธ€ 0