Ver3.0 ๐ฏ ํ ์คํ ํ๋ ์์ํฌ์ TDD, ์ด์ ์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ์์๋ณด์!

๐ฏ ํ ์คํ ํ๋ ์์ํฌ์ TDD, ์ด์ ์น๊ตฌ์ฒ๋ผ ์ฝ๊ฒ ์์๋ณด์!
JUnit, TestNG, Mockito๋ก ์์ํ๋ ํ ์คํธ ์ฃผ๋ ๊ฐ๋ฐ์ ๋ชจ๋ ๊ฒ
๐ ์์ํ๊ธฐ ์ ์: ์ ํ ์คํธ๊ฐ ์ค์ํ ๊น?
์๋
! ์ค๋์ ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ํ
์คํ
ํ๋ ์์ํฌ์ TDD(Test-Driven Development)์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ๐
ํน์ ์ด๋ฐ ๊ฒฝํ ์์ด? ์ฝ๋๋ฅผ ์ด์ฌํ ์ง๊ณ ๋์ ์คํํ๋๋ฐ ์์์น ๋ชปํ ๋ฒ๊ทธ๊ฐ ํ์ด๋์์ ๋ฐค์ ๋๋ฒ๊น
ํ ์ ๋ง์ด์ผ. ์๋๋ฉด ๋ค๋ฅธ ์ฌ๋์ด ์ง ์ฝ๋๋ฅผ ์์ ํ๋๋ฐ, ์ด๋์ ๊ฐ ์ค๋ฅ๊ฐ ๋ฐ์ํด์ ๋นํฉํ๋ ๊ฒฝํ? ๐คฏ
๋ฐ๋ก ์ด๋ฐ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ ํ
์คํ
ํ๋ ์์ํฌ์ TDD ๋ฐฉ๋ฒ๋ก ์ด์ผ. ์ด๊ฑด ๋จ์ํ ๋ฒ๊ทธ๋ฅผ ์ฐพ๋ ๋๊ตฌ๊ฐ ์๋๋ผ, ์ฝ๋์ ํ์ง์ ๋์ด๊ณ ๊ฐ๋ฐ์์ ์์ ๊ฐ์ ํค์์ฃผ๋ ๊ฐ๋ ฅํ ๋ฌด๊ธฐ์ง!
ํ ์คํธ ์ฝ๋๋ ๋ณดํ์ด์ผ. ๋น์ฅ์ ์๊ฐ์ด ๋ ๊ฑธ๋ฆฌ๋ ๊ฒ ๊ฐ์ง๋ง, ๋์ค์ ๋ฐ์ํ ์ ์๋ ์์ฒญ๋ ์๊ฐ ๋ญ๋น์ ์คํธ๋ ์ค๋ฅผ ๋ง์์ฃผ๋ ์ต๊ณ ์ ํฌ์๋ผ๊ณ ํ ์ ์์ด.
๐ 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๋ ๋จ์ํ ํ
์คํธ ์ฝ๋ ์์ฑ๋ฒ๋ง ๋ฐฐ์ฐ๋ฉด ๋๋ ๊ฒ ์๋์ผ. ์ฌ๊ณ ๋ฐฉ์ ์์ฒด๋ฅผ ๋ฐ๊ฟ์ผ ํ๊ฑฐ๋ . ์ด๊ฒ ์๊ฐ๋ณด๋ค ์ฝ์ง ์์.
๐จ 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 ์ฃผ์ ์ด๋ ธํ ์ด์
ํ ์คํธ ๋ฉ์๋๋ฅผ ๋ํ๋ด๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ด๋ ธํ ์ด์ ์ด์ผ. ์ด ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ๋ฉ์๋๋ ํ ์คํธ๋ก ์ธ์๋ผ.
@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 ํด๋์ค๋ ๋ค์ํ ๊ฒ์ฆ ๋ฉ์๋๋ฅผ ์ ๊ณตํด. ์์ฃผ ์ฌ์ฉํ๋ ๊ฒ๋ค์ ์ ๋ฆฌํด๋ณผ๊ฒ:
๋ ๊ฐ์ด ๊ฐ์์ง ํ์ธํด. ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ด๊ณ ๋ง์ด ์ฌ์ฉํ๋ ๋ฉ์๋์ผ.
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;
}
๐ญ TestNG: ๋ ๊ฐ๋ ฅํ ํ ์คํ ํ๋ ์์ํฌ
TestNG๋ "Testing Next Generation"์ ์ฝ์์ผ. JUnit์์ ์๊ฐ์ ๋ฐ์ ๋ง๋ค์ด์ก์ง๋ง, ๋ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํด. ํนํ ๋๊ท๋ชจ ํ ์คํธ ์ค์ํธ๋ฅผ ๊ด๋ฆฌํ๊ฑฐ๋ ๋ณต์กํ ํ ์คํธ ์๋๋ฆฌ์ค๋ฅผ ๊ตฌ์ฑํ ๋ ๋น์ ๋ฐํด. ๐
๐ JUnit vs TestNG: ๋ฌด์์ด ๋ค๋ฅผ๊น?
๋ง์ ์ฌ๋๋ค์ด "JUnit์ด ์๋๋ฐ ์ 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 ์ฌ์ฉ ์ ์ฃผ์์ฌํญ
๋ชจ๋ ๊ฑธ ๋ชจํนํ๋ฉด ํ ์คํธ๊ฐ ์ค์ ๋์๊ณผ ๋๋จ์ด์ง ์ ์์ด. ์ ๋ง ํ์ํ ๋ถ๋ถ๋ง ๋ชจํนํ์.
2. final ํด๋์ค/๋ฉ์๋ ๋ชจํน
๊ธฐ๋ณธ์ ์ผ๋ก Mockito๋ final ํด๋์ค๋ ๋ฉ์๋๋ฅผ ๋ชจํนํ ์ ์์ด. mockito-inline ์์กด์ฑ์ ์ถ๊ฐํ๋ฉด ๊ฐ๋ฅํ๊ธด ํด.
3. static ๋ฉ์๋ ๋ชจํน
Mockito 3.4.0๋ถํฐ static ๋ฉ์๋ ๋ชจํน์ด ๊ฐ๋ฅํด์ก์ง๋ง, ๊ฐ๋ฅํ๋ฉด ํผํ๋ ๊ฒ ์ข์. ์ค๊ณ๋ฅผ ๊ฐ์ ํ๋ ๊ฒ ๋ ๋์.
4. ๊ฒ์ฆ ์์
verify๋ ํ ์คํธ์ ๋ง์ง๋ง์ ํ๋ ๊ฒ ์ข์. ์ค๊ฐ์ ํ๋ฉด ์ฝ๋ ํ๋ฆ์ด ๋ณต์กํด์ ธ.
๐ง ์ค์ ํ๋ก์ ํธ์ TDD ์ ์ฉํ๊ธฐ
์ด๋ก ์ ์ถฉ๋ถํ ๋ฐฐ์ ์ผ๋, ์ด์ ์ค์ ํ๋ก์ ํธ์ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง ์์๋ณด์! ๋ง์ ๊ฐ๋ฐ์๋ค์ด TDD๋ฅผ ๋ฐฐ์ฐ๊ณ ๋ ์ค์ ์์ ๋ง๋งํดํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์. ๊ทธ๋์ ๋จ๊ณ๋ณ๋ก ์์ธํ ์ค๋ช ํด์ค๊ฒ. ๐
๐ฌ ํ๋ก์ ํธ ์์ํ๊ธฐ
๊ฐ๋จํ ๋์ ๊ด๋ฆฌ ์์คํ ์ TDD๋ก ๋ง๋ค์ด๋ณด์. ์ฑ ์ ์ถ๊ฐํ๊ณ , ์กฐํํ๊ณ , ๋์ฌํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํ ๊ฑฐ์ผ.
๋จผ์ ๋ฌด์์ ๋ง๋ค์ง ๋ช
ํํ ํด์ผ ํด. ์๊ตฌ์ฌํญ์ ์์ ๋จ์๋ก ์ชผ๊ฐ๋ณด์:
๐ ์ฑ
์ถ๊ฐ ๊ธฐ๋ฅ
- ์ ๋ชฉ, ์ ์, ISBN์ ์
๋ ฅ๋ฐ์ ์ฑ
์ ์ถ๊ฐํ๋ค
- ๊ฐ์ ISBN์ ์ฑ
์ ์ค๋ณต ์ถ๊ฐํ ์ ์๋ค
- ํ์ ์ ๋ณด๊ฐ ๋๋ฝ๋๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค
๐ ์ฑ
์กฐํ ๊ธฐ๋ฅ
- ISBN์ผ๋ก ์ฑ
์ ์กฐํํ ์ ์๋ค
- ์กด์ฌํ์ง ์๋ ์ฑ
์ ์กฐํํ๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค
- ๋ชจ๋ ์ฑ
๋ชฉ๋ก์ ์กฐํํ ์ ์๋ค
๐ ์ฑ
๋์ฌ ๊ธฐ๋ฅ
- ์ฌ์ฉ์๊ฐ ์ฑ
์ ๋์ฌํ ์ ์๋ค
- ์ด๋ฏธ ๋์ฌ ์ค์ธ ์ฑ
์ ๋์ฌํ ์ ์๋ค
- ๋์ฌ ๊ธฐ๋ก์ ๋จ๊ธด๋ค
๊ฐ์ฅ ๊ฐ๋จํ ๊ธฐ๋ฅ๋ถํฐ ์์ํ์. ์ฑ ์ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ์ ํ ์คํธ๋ฅผ ๋จผ์ ์์ฑํด.
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 ๋จ๊ณ์ผ. ๐ด
์ด์ ํ ์คํธ๋ฅผ ํต๊ณผ์ํค๊ธฐ ์ํ ์ต์ํ์ ์ฝ๋๋ฅผ ์์ฑํ์.
// 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);
}
}
ํ ์คํธ๋ฅผ ์คํํ๋ฉด ํต๊ณผํ ๊ฑฐ์ผ! ๐ข ํ์ง๋ง ์์ง ๊ฒ์ฆ ๋ก์ง์ด ์์ด. ๋ค์ ํ ์คํธ๋ฅผ ์์ฑํ์.
@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์ ํ์์
๋๋ค");
}
}
}
์ค์ ํ๋ก์ ํธ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ ๊ฑฐ์ผ. 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๏ธโฃ ํ ์คํธ๋ ์๊ณ ๋น ๋ฅด๊ฒ
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: ๋ฌด์์ ํ ์คํธํด์ผ ํ ์ง ๋ชจ๋ฅด๊ฒ ์ด์
๋ฌธ์ : "ํ
์คํธ๋ฅผ ์์ฑํ๊ณ ์ถ์๋ฐ ์ด๋์๋ถํฐ ์์ํด์ผ ํ ์ง ๋ง๋งํด์."
ํด๊ฒฐ์ฑ
:
์ด๋ฐ ์์๋ก ์๊ฐํด๋ด:
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ฑ๊ณต ์๋๋ฆฌ์ค๋ถํฐ ํ ์คํธํด.
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

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