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