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