π Mocking
π What is Mocking?β
Mocking is a technique that uses fake objects instead of real objects in tests. You can remove external dependencies (DB, API, etc.) and make tests fast and stable!
π‘ MockK Setupβ
build.gradle.ktsβ
dependencies {
testImplementation("io.mockk:mockk:1.13.5")
}
π― Basic Usageβ
Creating 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") }
}
}
Handling Multiple Callsβ
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)
}
}
π¨ Practical Examplesβ
Testing 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)
}
}
Testing API Clientβ
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") }
}
}
π§ Advanced Featuresβ
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)
}
}
π₯ Practical Patternsβ
Testing Exception Handlingβ
class ExceptionTest {
@Test
fun `μμΈ λ°μ ν
μ€νΈ`() {
val repo = mockk<UserRepository>()
every { repo.findById("error") } throws Exception("DB μ€λ₯")
val service = UserService(repo)
assertFailsWith<Exception> {
service.getUserName("error")
}
}
}
Sequential Callsβ
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"))
}
}
Conditional 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 Functionsβ
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("μ λ°μ΄ν°") }
}
}
π― Verification Patternsβ
Call Countβ
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()) }
}
}
Verify Orderβ
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())
}
}
}
π€ Frequently Asked Questionsβ
Q1. What's the difference between Mock and Spy?β
A: Mock is fake, Spy is real + partially fake!
// 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. What is any()?β
A: A matcher that accepts any 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. When should I use Relaxed Mock?β
A: When you don't want to define every method!
// β μΌμΌμ΄ μ μ
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β
Create independent tests with Mocking!
Key Takeaways:
β
Create Mock with mockk
β
Define behavior with every
β
Verify calls with verify
β
Support coroutines with coEvery
β
Convenient with Relaxed Mock
Next Step: Learn test-driven development in TDD!