🎭 Mocking
📖 Mockingとは?
Mockingとは、テストで本物のオブジェクトの代わりに偽のオブジェクトを使用する技法です。外部依存関係(DB、APIなど)を除去して、テストを高速かつ安定させることができます!
💡 MockKのセットアップ
build.gradle.kts
dependencies {
testImplementation("io.mockk:mockk:1.13.5")
}
🎯 基本的な使い方
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") }
}
}
複数の呼び出しの処理
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)
}
}
🎨 実践例
サービスのテスト
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クライアントのテスト
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") }
}
}
🔧 高度な機能
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)
}
}
🔥 実用的なパターン
例外処理のテスト
class ExceptionTest {
@Test
fun `예외 발생 테스트`() {
val repo = mockk<UserRepository>()
every { repo.findById("error") } throws Exception("DB 오류")
val service = UserService(repo)
assertFailsWith<Exception> {
service.getUserName("error")
}
}
}
連続呼び出し
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
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
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("새 데이터") }
}
}
🎯 検証パターン
呼び出し回数
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()) }
}
}
順序の検証
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())
}
}
}
🤔 よくある質問
Q1. MockとSpyの違いは?
A: Mockは偽物、Spyは本物+部分的に偽物です!
// 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. any()とは何ですか?
A: どんな引数でも受け入れるmatcherです!
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. Relaxed Mockはいつ使いますか?
A: すべてのメソッドを定義するのが面倒なときです!
// ❌ 일일이 정의
val repo = mockk<UserRepository>()
every { repo.findById(any()) } returns null
every { repo.save(any()) } returns User("1", "test")
// ✅ 자동으로 기본값
val repo = mockk<UserRepository>(relaxed = true)
// 정의 안 해도 기본값 반환
🎬 まとめ
Mockingで独立したテストを作成しましょう!
要点まとめ:
✅ mockkでMockを作成
✅ everyで動作を定義
✅ verifyで呼び出しを検証
✅ coEveryでコルーチンをサポート
✅ Relaxed Mockで便利に
次のステップ:TDDでテスト駆動開発を学びましょう!