Passer au contenu principal

✅ Tests Unitaires

📖 Qu'est-ce qu'un Test Unitaire?

Les Tests Unitaires sont des tests qui vérifient si de petites unités de code (fonctions, classes) fonctionnent correctement. Ils vous permettent de trouver rapidement des bugs et de refactoriser en toute sécurité!

💡 Configuration de Base

build.gradle.kts

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

tasks.test {
useJUnitPlatform()
}

🎯 Votre Premier Test

Test de Fonction Simple

// 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)
}
}

🔧 Structure de Test

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 et 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)

🎨 Diverses Assertions

Vérifications de Base

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)
}
}

Tests d'Exceptions

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()
}

🔥 Exemples Pratiques

Test d'Utilitaire de Chaînes

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"))
}
}

Test de Traitement de Listes

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))
}
}

Test de Logique Métier

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())
}
}

🎯 Tests Paramétrés

Plusieurs Cas à la Fois

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))
}
}

🛠️ Conseils de Test

Noms de Test Significatifs

class GoodTestNames {
// ❌ 나쁜 예
@Test
fun test1() { }

// ✅ 좋은 예
@Test
fun `이메일 형식이 올바르지 않으면 예외가 발생한다`() { }

@Test
fun `장바구니에 아이템 추가시 개수가 증가한다`() { }
}

Tests Indépendants

// ❌ 나쁜 예 - 테스트 순서에 의존
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)
}
}

🤔 Questions Fréquentes

Q1. Dois-je tester chaque fonction?

R: Commencez par la logique métier importante!

// ✅ 테스트 필요
fun calculateDiscount(price: Double): Double

// ❌ 테스트 불필요 (단순 getter)
val name: String get() = _name

Q2. Comment tester les fonctions privées?

R: Elles sont généralement testées indirectement via les fonctions publiques!

class Processor {
fun process(data: String): String {
return clean(data).uppercase() // private 함수 간접 테스트
}

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

Q3. Que faire si le code de test est trop long?

R: Créez des fonctions d'aide pour la réutilisation!

class TestHelpers {
fun createTestUser(name: String = "테스트") = User(name)

fun createTestOrder(itemCount: Int = 1) = Order(
items = List(itemCount) { Item("상품$it", 1000.0, 1) }
)
}

🎬 Conclusion

Code stable grâce aux tests unitaires!

핵심 정리:
✅ @Test로 테스트 작성
✅ assertEquals로 검증
✅ Given-When-Then 패턴
✅ 독립적인 테스트 유지
✅ 의미있는 테스트 이름

Prochaine Étape: Apprenez des méthodes de vérification plus puissantes dans Assertion!