Skip to main content

πŸ”„ TDD (Test-Driven Development)

πŸ“– What is TDD?​

TDD is a development methodology where you write tests first and then write code that passes those tests. You can reduce bugs and improve design!

πŸ’‘ TDD Cycle​

Red-Green-Refactor​

1. πŸ”΄ Red: Write a failing test
2. 🟒 Green: Write minimal code to pass the test
3. πŸ”΅ Refactor: Improve the code

🎯 First TDD Example​

Creating a Calculator​

import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe

// 1️⃣ Red: Write test first
class CalculatorTest : StringSpec({
"2와 3을 λ”ν•˜λ©΄ 5κ°€ λœλ‹€" {
val calculator = Calculator()
calculator.add(2, 3) shouldBe 5
}
})

// 컴파일 μ—λŸ¬! Calculatorκ°€ μ—†μŒ

// 2️⃣ Green: Minimal code
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
}

// 3️⃣ Refactor: κ°œμ„ ν•  λΆ€λΆ„ μ—†μŒ (κ°„λ‹¨ν•˜λ―€λ‘œ)

String Processing​

// 1️⃣ Red: Write test
class StringProcessorTest : StringSpec({
"빈 λ¬Έμžμ—΄μ€ 빈 λ¬Έμžμ—΄μ„ λ°˜ν™˜ν•œλ‹€" {
val processor = StringProcessor()
processor.reverse("") shouldBe ""
}

"helloλ₯Ό λ’€μ§‘μœΌλ©΄ ollehκ°€ λœλ‹€" {
val processor = StringProcessor()
processor.reverse("hello") shouldBe "olleh"
}
})

// 2️⃣ Green: Implementation
class StringProcessor {
fun reverse(text: String): String {
return text.reversed()
}
}

// 3️⃣ Refactor: 더 λ‚˜μ€ 방법이 μžˆμ„κΉŒ?
// 이미 간결함!

🎨 Practical TDD​

Shopping Cart Development​

Step 1: Empty Cart​

// Red
class ShoppingCartTest : StringSpec({
"μƒˆ μΉ΄νŠΈλŠ” μ•„μ΄ν…œμ΄ 0κ°œλ‹€" {
val cart = ShoppingCart()
cart.getItemCount() shouldBe 0
}
})

// Green
class ShoppingCart {
private val items = mutableListOf<Item>()

fun getItemCount(): Int = items.size
}

data class Item(val name: String, val price: Double)

Step 2: Adding Items​

// Red
"μ•„μ΄ν…œμ„ μΆ”κ°€ν•˜λ©΄ κ°œμˆ˜κ°€ μ¦κ°€ν•œλ‹€" {
val cart = ShoppingCart()
cart.addItem(Item("사과", 1000.0))

cart.getItemCount() shouldBe 1
}

// Green
class ShoppingCart {
private val items = mutableListOf<Item>()

fun addItem(item: Item) {
items.add(item)
}

fun getItemCount(): Int = items.size
}

Step 3: Calculating Total​

// Red
"총앑을 κ³„μ‚°ν•œλ‹€" {
val cart = ShoppingCart()
cart.addItem(Item("사과", 1000.0))
cart.addItem(Item("λ°”λ‚˜λ‚˜", 1500.0))

cart.getTotal() shouldBe 2500.0
}

// Green
class ShoppingCart {
private val items = mutableListOf<Item>()

fun addItem(item: Item) {
items.add(item)
}

fun getItemCount(): Int = items.size

fun getTotal(): Double {
return items.sumOf { it.price }
}
}

// Refactor: 깔끔함!

πŸ”₯ Business Logic TDD​

Discount Calculator​

// Red: Test first
class DiscountCalculatorTest : StringSpec({
"10% 할인" {
val calculator = DiscountCalculator()
calculator.calculate(10000.0, 0.1) shouldBe 9000.0
}

"ν• μΈμœ¨μ΄ 0이면 원가" {
val calculator = DiscountCalculator()
calculator.calculate(10000.0, 0.0) shouldBe 10000.0
}

"ν• μΈμœ¨μ΄ 1이면 0원" {
val calculator = DiscountCalculator()
calculator.calculate(10000.0, 1.0) shouldBe 0.0
}
})

// Green: Implementation
class DiscountCalculator {
fun calculate(price: Double, rate: Double): Double {
require(rate in 0.0..1.0) { "ν• μΈμœ¨μ€ 0~1 사이여야 ν•©λ‹ˆλ‹€" }
return price * (1 - rate)
}
}

// Refactor: 검증 둜직 μΆ”κ°€ν–ˆμœΌλ―€λ‘œ ν…ŒμŠ€νŠΈ μΆ”κ°€
"잘λͺ»λœ ν• μΈμœ¨μ€ μ˜ˆμ™Έ λ°œμƒ" {
val calculator = DiscountCalculator()

shouldThrow<IllegalArgumentException> {
calculator.calculate(10000.0, 1.5)
}
}

User Registration​

// Red
class UserRegistrationTest : StringSpec({
"μœ νš¨ν•œ μ‚¬μš©μž 등둝" {
val service = UserRegistration()
val user = service.register("hong@example.com", "Pass123!")

user.email shouldBe "hong@example.com"
}

"쀑볡 이메일은 μ‹€νŒ¨" {
val service = UserRegistration()
service.register("hong@example.com", "Pass123!")

shouldThrow<DuplicateEmailException> {
service.register("hong@example.com", "Pass456!")
}
}

"μ•½ν•œ λΉ„λ°€λ²ˆν˜ΈλŠ” μ‹€νŒ¨" {
val service = UserRegistration()

shouldThrow<WeakPasswordException> {
service.register("hong@example.com", "123")
}
}
})

// Green
class UserRegistration {
private val users = mutableMapOf<String, User>()

fun register(email: String, password: String): User {
if (users.containsKey(email)) {
throw DuplicateEmailException()
}

if (password.length < 8) {
throw WeakPasswordException()
}

val user = User(email, password)
users[email] = user
return user
}
}

data class User(val email: String, val password: String)

class DuplicateEmailException : Exception()
class WeakPasswordException : Exception()

// Refactor: 검증 둜직 뢄리
class UserRegistration {
private val users = mutableMapOf<String, User>()
private val validator = UserValidator()

fun register(email: String, password: String): User {
validator.validateEmail(email, users.keys)
validator.validatePassword(password)

val user = User(email, password)
users[email] = user
return user
}
}

class UserValidator {
fun validateEmail(email: String, existingEmails: Set<String>) {
if (existingEmails.contains(email)) {
throw DuplicateEmailException()
}
}

fun validatePassword(password: String) {
if (password.length < 8) {
throw WeakPasswordException()
}
}
}

🎯 TDD Best Practices​

Small Steps​

// ❌ Too much at once
class ComplexTest : StringSpec({
"μ‚¬μš©μž 등둝, 둜그인, ν”„λ‘œν•„ μˆ˜μ •κΉŒμ§€" {
// λ„ˆλ¬΄ 볡작!
}
})

// βœ… One at a time
class SimpleTest : StringSpec({
"μ‚¬μš©μž 등둝" {
// ν•˜λ‚˜λ§Œ ν…ŒμŠ€νŠΈ
}

"둜그인" {
// λ³„λ„λ‘œ ν…ŒμŠ€νŠΈ
}

"ν”„λ‘œν•„ μˆ˜μ •" {
// 또 λ³„λ„λ‘œ
}
})

Clear Test Names​

class GoodNaming : StringSpec({
// ❌ Bad name
"test1" { }

// βœ… Good name
"빈 μž₯λ°”κ΅¬λ‹ˆμ˜ 총앑은 0원이닀" { }
"ν• μΈμœ¨ 10%λ₯Ό μ μš©ν•˜λ©΄ 10% ν• μΈλœλ‹€" { }
"쀑볡 μ΄λ©”μΌλ‘œ κ°€μž…ν•˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€" { }
})

Given-When-Then Pattern​

class GWTTest : StringSpec({
"μ£Όλ¬Έ 생성 ν›„ κ²°μ œν•˜λ©΄ μƒνƒœκ°€ PAIDκ°€ λœλ‹€" {
// Given
val order = Order(items = listOf(Item("μƒν’ˆ", 10000.0)))

// When
order.pay()

// Then
order.status shouldBe OrderStatus.PAID
}
})

enum class OrderStatus { PENDING, PAID, SHIPPED }

data class Order(
val items: List<Item>,
var status: OrderStatus = OrderStatus.PENDING
) {
fun pay() {
status = OrderStatus.PAID
}
}

πŸ›‘οΈ Benefits of TDD​

1. Early Bug Detection​

// Writing tests first clarifies requirements
"음수 κΈˆμ•‘μ€ ν—ˆμš©ν•˜μ§€ μ•ŠλŠ”λ‹€" {
shouldThrow<IllegalArgumentException> {
Order(amount = -1000.0)
}
}

// Validation logic is naturally added during implementation
data class Order(val amount: Double) {
init {
require(amount >= 0) { "κΈˆμ•‘μ€ 0 이상이어야 ν•©λ‹ˆλ‹€" }
}
}

2. Safety Net for Refactoring​

// With tests, you can refactor with confidence
class BeforeRefactor {
fun calculate(price: Double, quantity: Int): Double {
var total = price * quantity
if (quantity > 10) {
total = total * 0.9 // 10개 이상 10% 할인
}
return total
}
}

// Refactoring
class AfterRefactor {
fun calculate(price: Double, quantity: Int): Double {
val subtotal = price * quantity
val discount = getDiscount(quantity)
return subtotal * (1 - discount)
}

private fun getDiscount(quantity: Int): Double {
return if (quantity > 10) 0.1 else 0.0
}
}

// ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•˜λ©΄ λ¦¬νŒ©ν† λ§ 성곡!

πŸ€” Frequently Asked Questions​

Q1. Do I always have to write tests first?​

A: Apply it first to complex logic or important features!

// βœ… TDD required
// - Business logic
// - Money calculations
// - Validation logic

// ❌ TDD optional
// - Simple getters/setters
// - UI layout
// - Configuration files

Q2. How many tests should I write?​

A: Include boundary values and exception cases!

class ComprehensiveTest : StringSpec({
// Normal case
"정상적인 μž…λ ₯" { }

// Boundary values
"μ΅œμ†Œκ°’" { }
"μ΅œλŒ€κ°’" { }

// Exceptions
"null μž…λ ₯" { }
"빈 λ¬Έμžμ—΄" { }
"음수" { }
})

Q3. Can I apply TDD to legacy code?​

A: Start with the parts you're modifying!

// 1. κΈ°μ‘΄ κΈ°λŠ₯에 ν…ŒμŠ€νŠΈ μΆ”κ°€
// 2. ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•˜λŠ”μ§€ 확인
// 3. λ¦¬νŒ©ν† λ§
// 4. μƒˆ κΈ°λŠ₯은 TDD둜

🎬 Conclusion​

Write robust code with TDD!

Key Takeaways:
βœ… Red-Green-Refactor cycle
βœ… Write tests first
βœ… Proceed in small steps
βœ… Safety net for refactoring
βœ… Design improvement effect

Congratulations! You've completed the Testing series! πŸŽ‰

Next Step: Start backend development with Ktor Introduction!