π 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!