Skip to main content

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