Passer au contenu principal

🎭 Mocking

📖 Qu'est-ce que le Mocking ?

Le Mocking est une technique qui utilise de faux objets au lieu d'objets réels dans les tests. Vous pouvez supprimer les dépendances externes (DB, API, etc.) et rendre les tests rapides et stables !

💡 Configuration de MockK

build.gradle.kts

dependencies {
testImplementation("io.mockk:mockk:1.13.5")
}

🎯 Utilisation de Base

Créer un Mock

import io.mockk.*
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

interface UserRepository {
fun findById(id: String): User?
fun save(user: User): User
}

data class User(val id: String, val name: String)

class BasicMockTest {
@Test
fun `Mock 기본 사용`() {
// Mock 생성
val repo = mockk<UserRepository>()

// 동작 정의
every { repo.findById("123") } returns User("123", "홍길동")

// 테스트
val user = repo.findById("123")
assertEquals("홍길동", user?.name)

// 검증
verify { repo.findById("123") }
}
}

Gérer Plusieurs Appels

class MultipleCalls {
@Test
fun `여러 번 호출`() {
val repo = mockk<UserRepository>()

// 첫 번째는 null, 두 번째는 User 반환
every { repo.findById("123") } returnsMany listOf(
null,
User("123", "홍길동")
)

assertEquals(null, repo.findById("123"))
assertEquals("홍길동", repo.findById("123")?.name)
}
}

🎨 Exemples Pratiques

Tester le Service

class UserService(private val repository: UserRepository) {
fun getUserName(id: String): String {
val user = repository.findById(id)
return user?.name ?: "Unknown"
}

fun createUser(name: String): User {
val user = User(generateId(), name)
return repository.save(user)
}

private fun generateId() = "ID-${System.currentTimeMillis()}"
}

class UserServiceTest {
@Test
fun `사용자 조회 성공`() {
// Given
val repo = mockk<UserRepository>()
every { repo.findById("123") } returns User("123", "홍길동")

val service = UserService(repo)

// When
val name = service.getUserName("123")

// Then
assertEquals("홍길동", name)
verify { repo.findById("123") }
}

@Test
fun `사용자 없을 때`() {
val repo = mockk<UserRepository>()
every { repo.findById("999") } returns null

val service = UserService(repo)
val name = service.getUserName("999")

assertEquals("Unknown", name)
}
}

Tester le Client API

interface ApiClient {
suspend fun fetchData(url: String): String
}

class DataService(private val client: ApiClient) {
suspend fun loadData(): String {
return client.fetchData("https://api.example.com/data")
}
}

class DataServiceTest {
@Test
fun `API 호출 테스트`() = runBlocking {
// Given
val client = mockk<ApiClient>()
coEvery { client.fetchData(any()) } returns "데이터"

val service = DataService(client)

// When
val result = service.loadData()

// Then
assertEquals("데이터", result)
coVerify { client.fetchData("https://api.example.com/data") }
}
}

🔧 Fonctionnalités Avancées

Relaxed Mock

class RelaxedMockTest {
@Test
fun `Relaxed Mock`() {
// 모든 메서드가 기본값 반환
val repo = mockk<UserRepository>(relaxed = true)

// 정의하지 않아도 null 반환
val user = repo.findById("123")
assertEquals(null, user)
}
}

Spy

class RealUserRepository : UserRepository {
override fun findById(id: String): User? {
return User(id, "실제 사용자")
}

override fun save(user: User): User {
return user
}
}

class SpyTest {
@Test
fun `Spy 사용`() {
val repo = spyk(RealUserRepository())

// 일부만 mock
every { repo.findById("123") } returns User("123", "Mock 사용자")

// Mock된 메서드
assertEquals("Mock 사용자", repo.findById("123")?.name)

// 실제 메서드
assertEquals("실제 사용자", repo.findById("456")?.name)
}
}

Capture

class CaptureTest {
@Test
fun `인자 캡처`() {
val repo = mockk<UserRepository>(relaxed = true)
val slot = slot<User>()

val service = UserService(repo)

every { repo.save(capture(slot)) } returns User("1", "홍길동")

service.createUser("홍길동")

// 캡처된 인자 검증
assertEquals("홍길동", slot.captured.name)
}
}

🔥 Modèles Pratiques

Tester la Gestion des Exceptions

class ExceptionTest {
@Test
fun `예외 발생 테스트`() {
val repo = mockk<UserRepository>()

every { repo.findById("error") } throws Exception("DB 오류")

val service = UserService(repo)

assertFailsWith<Exception> {
service.getUserName("error")
}
}
}

Appels Séquentiels

class SequentialTest {
@Test
fun `연속 호출 테스트`() {
val repo = mockk<UserRepository>()

every { repo.findById("123") } returns User("123", "첫번째") andThen
User("123", "두번째") andThen
null

assertEquals("첫번째", repo.findById("123")?.name)
assertEquals("두번째", repo.findById("123")?.name)
assertEquals(null, repo.findById("123"))
}
}

Mock Conditionnel

class ConditionalTest {
@Test
fun `조건부 동작`() {
val repo = mockk<UserRepository>()

every {
repo.findById(match { it.startsWith("VIP") })
} returns User("VIP-001", "VIP 사용자")

every {
repo.findById(match { !it.startsWith("VIP") })
} returns null

assertEquals("VIP 사용자", repo.findById("VIP-001")?.name)
assertEquals(null, repo.findById("123"))
}
}

🛠️ Mocking de Coroutines

Fonctions suspend

interface AsyncRepository {
suspend fun fetch(): String
suspend fun save(data: String)
}

class AsyncTest {
@Test
fun `suspend 함수 mock`() = runBlocking {
val repo = mockk<AsyncRepository>()

coEvery { repo.fetch() } returns "데이터"
coEvery { repo.save(any()) } just Runs

assertEquals("데이터", repo.fetch())
repo.save("새 데이터")

coVerify { repo.fetch() }
coVerify { repo.save("새 데이터") }
}
}

🎯 Modèles de Vérification

Nombre d'Appels

class VerifyTest {
@Test
fun `호출 횟수 검증`() {
val repo = mockk<UserRepository>(relaxed = true)

repo.findById("123")
repo.findById("123")
repo.findById("456")

// 정확히 2번
verify(exactly = 2) { repo.findById("123") }

// 최소 1번
verify(atLeast = 1) { repo.findById("456") }

// 최대 3번
verify(atMost = 3) { repo.findById(any()) }
}
}

Vérifier l'Ordre

class OrderTest {
@Test
fun `호출 순서 검증`() {
val repo = mockk<UserRepository>(relaxed = true)

repo.findById("1")
repo.findById("2")
repo.save(User("3", "test"))

verifyOrder {
repo.findById("1")
repo.findById("2")
repo.save(any())
}
}
}

🤔 Questions Fréquemment Posées

Q1. Quelle est la différence entre Mock et Spy ?

R : Mock est faux, Spy est réel + partiellement faux !

// Mock - 모든 동작 정의 필요
val mock = mockk<UserRepository>()
every { mock.findById(any()) } returns null

// Spy - 실제 객체 + 일부만 mock
val spy = spyk(RealUserRepository())
every { spy.findById("특정ID") } returns User("특정ID", "Mock")
// 나머지는 실제 동작

Q2. Qu'est-ce que any() ?

R : Un matcher qui accepte n'importe quel argument !

val repo = mockk<UserRepository>()

// 아무 String이나
every { repo.findById(any()) } returns User("1", "test")

// 특정 패턴
every { repo.findById(match { it.startsWith("VIP") }) } returns User("vip", "VIP")

Q3. Quand utiliser Relaxed Mock ?

R : Quand vous ne voulez pas définir toutes les méthodes !

// ❌ 일일이 정의
val repo = mockk<UserRepository>()
every { repo.findById(any()) } returns null
every { repo.save(any()) } returns User("1", "test")

// ✅ 자동으로 기본값
val repo = mockk<UserRepository>(relaxed = true)
// 정의 안 해도 기본값 반환

🎬 Conclusion

Créez des tests indépendants avec le Mocking !

Points Clés :
✅ Créer Mock avec mockk
✅ Définir le comportement avec every
✅ Vérifier les appels avec verify
✅ Prendre en charge les coroutines avec coEvery
✅ Pratique avec Relaxed Mock

Prochaine Étape : Découvrez le développement piloté par les tests dans TDD !