🎭 Mocking
📖 Was ist Mocking?
Mocking ist eine Technik, bei der im Test anstelle echter Objekte gefälschte Objekte verwendet werden. Sie können externe Abhängigkeiten (DB, API usw.) entfernen und Tests schnell und stabil gestalten!
💡 MockK-Einrichtung
build.gradle.kts
dependencies {
testImplementation("io.mockk:mockk:1.13.5")
}
🎯 Grundlegende Verwendung
Mock erstellen
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") }
}
}
Mehrere Aufrufe verarbeiten
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)
}
}
🎨 Praxisbeispiele
Service testen
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)
}
}
API-Client testen
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") }
}
}
🔧 Erweiterte Funktionen
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)
}
}
🔥 Praktische Muster
Ausnahmebehandlung testen
class ExceptionTest {
@Test
fun `예외 발생 테스트`() {
val repo = mockk<UserRepository>()
every { repo.findById("error") } throws Exception("DB 오류")
val service = UserService(repo)
assertFailsWith<Exception> {
service.getUserName("error")
}
}
}
Aufeinanderfolgende Aufrufe
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"))
}
}
Bedingtes Mock
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"))
}
}
🛠️ Coroutine-Mocking
suspend-Funktionen
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("새 데이터") }
}
}
🎯 Verifikationsmuster
Aufrufhäufigkeit
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()) }
}
}
Reihenfolge verifizieren
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())
}
}
}
🤔 Häufig gestellte Fragen
F1. Was ist der Unterschied zwischen Mock und Spy?
A: Mock ist gefälscht, Spy ist echt + teilweise gefälscht!
// 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")
// 나머지는 실제 동작
F2. Was ist any()?
A: Ein Matcher, der jedes Argument akzeptiert!
val repo = mockk<UserRepository>()
// 아무 String이나
every { repo.findById(any()) } returns User("1", "test")
// 특정 패턴
every { repo.findById(match { it.startsWith("VIP") }) } returns User("vip", "VIP")
F3. Wann verwendet man Relaxed Mock?
A: Wenn man nicht alle Methoden definieren möchte!
// ❌ 일일이 정의
val repo = mockk<UserRepository>()
every { repo.findById(any()) } returns null
every { repo.save(any()) } returns User("1", "test")
// ✅ 자동으로 기본값
val repo = mockk<UserRepository>(relaxed = true)
// 정의 안 해도 기본값 반환
🎬 Abschluss
Erstellen Sie unabhängige Tests mit Mocking!
Kernzusammenfassung:
✅ Mock mit mockk erstellen
✅ Verhalten mit every definieren
✅ Aufrufe mit verify verifizieren
✅ Coroutines mit coEvery unterstützen
✅ Bequem mit Relaxed Mock
Nächster Schritt: Lernen Sie testgetriebene Entwicklung in TDD!