Skip to main content

βœ… Unit Testing

πŸ“– What is Unit Testing?​

Unit Testing is a test that verifies whether small units of code (functions, classes) work properly. It allows you to find bugs quickly and refactor safely!

πŸ’‘ Basic Setup​

build.gradle.kts​

dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.junit.jupiter:junit-jupiter:5.9.0")
}

tasks.test {
useJUnitPlatform()
}

🎯 Your First Test​

Simple Function Test​

// src/main/kotlin/Calculator.kt
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}

fun subtract(a: Int, b: Int): Int {
return a - b
}
}

// src/test/kotlin/CalculatorTest.kt
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class CalculatorTest {
@Test
fun `λ§μ…ˆ ν…ŒμŠ€νŠΈ`() {
val calculator = Calculator()
val result = calculator.add(2, 3)
assertEquals(5, result)
}

@Test
fun `λΊ„μ…ˆ ν…ŒμŠ€νŠΈ`() {
val calculator = Calculator()
val result = calculator.subtract(5, 3)
assertEquals(2, result)
}
}

πŸ”§ Test Structure​

Given-When-Then​

class ShoppingCart {
private val items = mutableListOf<String>()

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

fun getItemCount(): Int = items.size
}

class ShoppingCartTest {
@Test
fun `μ•„μ΄ν…œμ„ μΆ”κ°€ν•˜λ©΄ κ°œμˆ˜κ°€ μ¦κ°€ν•œλ‹€`() {
// Given (μ€€λΉ„)
val cart = ShoppingCart()

// When (μ‹€ν–‰)
cart.addItem("사과")
cart.addItem("λ°”λ‚˜λ‚˜")

// Then (검증)
assertEquals(2, cart.getItemCount())
}
}

Setup and Teardown​

import org.junit.jupiter.api.*

class UserServiceTest {
private lateinit var userService: UserService

@BeforeEach
fun setup() {
// 각 ν…ŒμŠ€νŠΈ 전에 μ‹€ν–‰
userService = UserService()
println("ν…ŒμŠ€νŠΈ μ‹œμž‘")
}

@AfterEach
fun teardown() {
// 각 ν…ŒμŠ€νŠΈ 후에 μ‹€ν–‰
println("ν…ŒμŠ€νŠΈ 끝")
}

@Test
fun `μ‚¬μš©μž 생성 ν…ŒμŠ€νŠΈ`() {
val user = userService.createUser("홍길동")
assertEquals("홍길동", user.name)
}

@Test
fun `μ‚¬μš©μž 쑰회 ν…ŒμŠ€νŠΈ`() {
userService.createUser("κΉ€μ² μˆ˜")
val user = userService.findUser("κΉ€μ² μˆ˜")
assertNotNull(user)
}
}

class UserService {
private val users = mutableMapOf<String, User>()

fun createUser(name: String): User {
val user = User(name)
users[name] = user
return user
}

fun findUser(name: String): User? = users[name]
}

data class User(val name: String)

🎨 Various Assertions​

Basic Assertions​

import kotlin.test.*

class AssertionTest {
@Test
fun `λ‹€μ–‘ν•œ assertion`() {
// 동등성
assertEquals(5, 2 + 3)
assertNotEquals(4, 2 + 3)

// null 검사
val value: String? = "hello"
assertNotNull(value)

val nullValue: String? = null
assertNull(nullValue)

// μ°Έ/κ±°μ§“
assertTrue(5 > 3)
assertFalse(5 < 3)

// μ»¬λ ‰μ…˜
val list = listOf(1, 2, 3)
assertTrue(list.contains(2))
assertEquals(3, list.size)
}
}

Exception Testing​

class ValidationTest {
@Test
fun `음수 μž…λ ₯μ‹œ μ˜ˆμ™Έ λ°œμƒ`() {
val exception = assertFailsWith<IllegalArgumentException> {
validateAge(-1)
}

assertEquals("λ‚˜μ΄λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€", exception.message)
}

@Test
fun `μœ νš¨ν•œ μž…λ ₯은 톡과`() {
assertDoesNotThrow {
validateAge(25)
}
}
}

fun validateAge(age: Int) {
require(age >= 0) { "λ‚˜μ΄λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€" }
}

inline fun <reified T : Throwable> assertFailsWith(block: () -> Unit): T {
try {
block()
throw AssertionError("μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€")
} catch (e: Throwable) {
if (e is T) return e
throw e
}
}

inline fun assertDoesNotThrow(block: () -> Unit) {
block()
}

πŸ”₯ Practical Examples​

String Utility Test​

object StringUtils {
fun reverse(text: String): String {
return text.reversed()
}

fun isPalindrome(text: String): Boolean {
val clean = text.lowercase().filter { it.isLetterOrDigit() }
return clean == clean.reversed()
}
}

class StringUtilsTest {
@Test
fun `λ¬Έμžμ—΄ λ’€μ§‘κΈ°`() {
assertEquals("olleh", StringUtils.reverse("hello"))
assertEquals("", StringUtils.reverse(""))
}

@Test
fun `회문 νŒλ³„`() {
assertTrue(StringUtils.isPalindrome("A man a plan a canal Panama"))
assertTrue(StringUtils.isPalindrome("race car"))
assertFalse(StringUtils.isPalindrome("hello"))
}
}

List Processing Test​

class ListProcessor {
fun filterEven(numbers: List<Int>): List<Int> {
return numbers.filter { it % 2 == 0 }
}

fun sum(numbers: List<Int>): Int {
return numbers.sum()
}
}

class ListProcessorTest {
private val processor = ListProcessor()

@Test
fun `짝수 필터링`() {
val numbers = listOf(1, 2, 3, 4, 5, 6)
val result = processor.filterEven(numbers)

assertEquals(listOf(2, 4, 6), result)
}

@Test
fun `빈 리슀트 처리`() {
val result = processor.filterEven(emptyList())
assertTrue(result.isEmpty())
}

@Test
fun `합계 계산`() {
val numbers = listOf(1, 2, 3, 4, 5)
assertEquals(15, processor.sum(numbers))
}
}

Business Logic Test​

data class Order(val items: List<Item>, val discount: Double = 0.0) {
fun calculateTotal(): Double {
val subtotal = items.sumOf { it.price * it.quantity }
return subtotal * (1 - discount)
}
}

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

class OrderTest {
@Test
fun `μ£Όλ¬Έ κΈˆμ•‘ 계산`() {
val order = Order(
items = listOf(
Item("사과", 1000.0, 2),
Item("λ°”λ‚˜λ‚˜", 1500.0, 1)
)
)

assertEquals(3500.0, order.calculateTotal())
}

@Test
fun `할인 적용`() {
val order = Order(
items = listOf(Item("μƒν’ˆ", 10000.0, 1)),
discount = 0.1 // 10% 할인
)

assertEquals(9000.0, order.calculateTotal())
}

@Test
fun `빈 주문`() {
val order = Order(emptyList())
assertEquals(0.0, order.calculateTotal())
}
}

🎯 Parameterized Tests​

Multiple Cases at Once​

import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource

class ParameterizedTests {
@ParameterizedTest
@CsvSource(
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
)
fun `λ§μ…ˆ ν…ŒμŠ€νŠΈ`(a: Int, b: Int, expected: Int) {
val calculator = Calculator()
assertEquals(expected, calculator.add(a, b))
}
}

πŸ› οΈ Testing Tips​

Meaningful Test Names​

class GoodTestNames {
// ❌ λ‚˜μœ 예
@Test
fun test1() { }

// βœ… 쒋은 예
@Test
fun `이메일 ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμœΌλ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€`() { }

@Test
fun `μž₯λ°”κ΅¬λ‹ˆμ— μ•„μ΄ν…œ μΆ”κ°€μ‹œ κ°œμˆ˜κ°€ μ¦κ°€ν•œλ‹€`() { }
}

Independent Tests​

// ❌ λ‚˜μœ 예 - ν…ŒμŠ€νŠΈ μˆœμ„œμ— 의쑴
class BadTest {
companion object {
var counter = 0
}

@Test
fun testA() {
counter++
assertEquals(1, counter)
}

@Test
fun testB() {
// testAκ°€ λ¨Όμ € μ‹€ν–‰λ˜μ–΄μ•Ό 톡과
assertEquals(2, ++counter)
}
}

// βœ… 쒋은 예 - 독립적
class GoodTest {
@Test
fun testA() {
val counter = 0
assertEquals(1, counter + 1)
}

@Test
fun testB() {
val counter = 1
assertEquals(2, counter + 1)
}
}

πŸ€” Frequently Asked Questions​

Q1. Do I need to test every function?​

A: Start with important business logic!

// βœ… ν…ŒμŠ€νŠΈ ν•„μš”
fun calculateDiscount(price: Double): Double

// ❌ ν…ŒμŠ€νŠΈ λΆˆν•„μš” (λ‹¨μˆœ getter)
val name: String get() = _name

Q2. How do I test private functions?​

A: They are usually tested indirectly through public functions!

class Processor {
fun process(data: String): String {
return clean(data).uppercase() // private ν•¨μˆ˜ κ°„μ ‘ ν…ŒμŠ€νŠΈ
}

private fun clean(text: String): String {
return text.trim()
}
}

Q3. What if the test code is too long?​

A: Create helper functions for reuse!

class TestHelpers {
fun createTestUser(name: String = "ν…ŒμŠ€νŠΈ") = User(name)

fun createTestOrder(itemCount: Int = 1) = Order(
items = List(itemCount) { Item("μƒν’ˆ$it", 1000.0, 1) }
)
}

🎬 Conclusion​

Stable code through unit testing!

핡심 정리:
βœ… @Test둜 ν…ŒμŠ€νŠΈ μž‘μ„±
βœ… assertEquals둜 검증
βœ… Given-When-Then νŒ¨ν„΄
βœ… 독립적인 ν…ŒμŠ€νŠΈ μœ μ§€
βœ… μ˜λ―ΈμžˆλŠ” ν…ŒμŠ€νŠΈ 이름

Next Step: Learn more powerful verification methods in Assertion!