MockK
MockK: Kotlin向けモッキングライブラリの完全ガイド
目次
- 序論
- MockKとは
- コアコンセプトと基礎
- インストールと設定
- 基本的な使用方法
- モックの詳細な制御
- スパイとPartialモック
- キャプチャとスロット
- 高度な機能
- アーキテクチャと実装の詳細
- ベストプラクティス
- トラブルシューティング
序論
Kotlinは、JVM上で動作する最新の言語として注目されています。その簡潔な構文と強力な型システムは、開発生産性を大幅に向上させます。しかし、ユニットテスト作成の際には、外部依存関係をモックする必要があります。MockKは、Kotlin専用に設計されたモッキングライブラリで、言語の特性を完全に活用して、直感的で強力なテスト環境を実現します。
本記事では、MockKの基本から高度な使用方法、アーキテクチャの詳細までを網羅します。設定例や実装パターンを多数提示し、実務レベルでの使用を想定した実践的な知識を提供します。
MockKとは
概要
MockKは、Kotlin言語専用に設計されたモッキングフレームワークです。Java向けのMockito、PowerMockなどのツールとは異なり、Kotlinの言語特性(データクラス、拡張関数、Coroutineなど)を完全にサポートしています。
主な特徴
- Kotlinネイティブ設計: Kotlinの構文やパターンに最適化
- 高度なモック機能: 関数、拡張関数、オブジェクトのモック化をシームレスに実現
- DSL形式のAPI: 読みやすく直感的なDSL(ドメイン固有言語)を提供
- Coroutineサポート: 非同期コードの効果的なテストが可能
- 詳細なAssertion: 呼び出しの検証や引数のキャプチャが容易
MockK vs その他のライブラリ
| 機能 | MockK | Mockito | PowerMock |
|---|---|---|---|
| Kotlin最適化 | ✓ | × | × |
| 拡張関数モック | ✓ | × | ○ |
| Objectモック | ✓ | × | ○ |
| Coroutineサポート | ✓ | × | × |
| オブジェクト検証 | ✓ | ○ | ○ |
| 学習曲線 | 低 | 中 | 高 |
コアコンセプトと基礎
モックとスパイの違い
モック(Mock): 完全に置き換えられたオブジェクト。すべての振る舞いが指定される。
val mockUserService: UserService = mockk()
every { mockUserService.getUser(1) } returns User(1, "John")
スパイ(Spy): 元のオブジェクトをラップしたもの。指定されていない呼び出しは本来の実装が実行される。
val spyUserService = spyk(UserService())
every { spyUserService.getUser(1) } returns User(1, "John")
// 他のメソッドは実装が実行される
Relaxedモック
Relaxedモックは、設定されていないメソッド呼び出しに対してデフォルト値を返します。
val relaxedMock: UserService = mockk(relaxed = true)
val result = relaxedMock.getUser(1) // デフォルト値が返される
オブジェクトのモック化
Kotlinのobjectシングルトンをモック化できます。
object LoggerFactory {
fun getLogger(): Logger = Logger()
}
mockkObject(LoggerFactory)
every { LoggerFactory.getLogger() } returns mockk()
インストールと設定
Gradle設定
dependencies {
// MockK本体
testImplementation("io.mockk:mockk:1.13.8")
// JUnit 4/5との連携
testImplementation("io.mockk:mockk-jvm:1.13.8")
// Android環境での使用
testImplementation("io.mockk:mockk-android:1.13.8")
debugImplementation("io.mockk:mockk-android:1.13.8")
}
// Kotlin Compiler Plugin for MockK(オプション)
plugins {
kotlin("plugin.all-open") version "1.9.0"
}
allOpen {
annotation("io.mockk.MockKDsl")
}
Maven設定
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<version>1.13.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-agent</artifactId>
<version>1.13.8</version>
<scope>test</scope>
</dependency>
テスト環境での初期化
import io.mockk.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.AfterEach
class UserServiceTest {
@BeforeEach
fun setUp() {
// テスト開始前にモックをリセット
clearAllMocks()
}
@AfterEach
fun tearDown() {
// テスト終了後にモックをクリア
unmockkAll()
}
@Test
fun testExample() {
// テストコード
}
}
JUnit 5での統合設定
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(MockKExtension::class)
class UserServiceTest {
@MockK
private lateinit var userRepository: UserRepository
@InjectMockKs
private lateinit var userService: UserService
@Test
fun testGetUser() {
every { userRepository.findById(1) } returns User(1, "John")
val result = userService.getUser(1)
assertEquals("John", result.name)
}
}
基本的な使用方法
シンプルなモック作成と検証
interface UserRepository {
fun findById(id: Int): User?
fun save(user: User): Boolean
fun deleteById(id: Int): Unit
}
data class User(val id: Int, val name: String, val email: String)
class UserService(private val repository: UserRepository) {
fun getUser(id: Int): User? = repository.findById(id)
fun createUser(name: String, email: String): Boolean {
val user = User(0, name, email)
return repository.save(user)
}
fun removeUser(id: Int) {
repository.deleteById(id)
}
}
class UserServiceTest {
@Test
fun testGetUserReturnsCorrectData() {
// Arrange
val mockRepository = mockk<UserRepository>()
val expectedUser = User(1, "John Doe", "john@example.com")
every { mockRepository.findById(1) } returns expectedUser
val userService = UserService(mockRepository)
// Act
val result = userService.getUser(1)
// Assert
assertEquals(expectedUser, result)
verify { mockRepository.findById(1) }
}
@Test
fun testCreateUserCallsSaveOnce() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.save(any()) } returns true
val userService = UserService(mockRepository)
val result = userService.createUser("Jane", "jane@example.com")
assertTrue(result)
verify(exactly = 1) { mockRepository.save(any()) }
}
@Test
fun testRemoveUserCallsDelete() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.deleteById(any()) } just Runs
val userService = UserService(mockRepository)
userService.removeUser(1)
verify { mockRepository.deleteById(1) }
}
}
複数の戻り値を指定
@Test
fun testMultipleReturns() {
val mockRepository = mockk<UserRepository>()
// 1回目の呼び出しで最初の値、以降は2番目の値を返す
every { mockRepository.findById(1) } returnsMany listOf(
User(1, "First", "first@example.com"),
User(1, "Second", "second@example.com")
)
val userService = UserService(mockRepository)
val first = userService.getUser(1)
val second = userService.getUser(1)
assertEquals("First", first?.name)
assertEquals("Second", second?.name)
}
例外をスロー
@Test
fun testExceptionHandling() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.findById(any()) } throws IllegalArgumentException("User not found")
val userService = UserService(mockRepository)
assertThrows<IllegalArgumentException> {
userService.getUser(1)
}
}
モックの詳細な制御
マッチャーの使用
interface PaymentService {
fun processPayment(amount: Double, currency: String): Boolean
fun refund(transactionId: String, amount: Double): Boolean
}
@Test
fun testPaymentProcessing() {
val mockPaymentService = mockk<PaymentService>()
// 正確な引数マッチ
every { mockPaymentService.processPayment(100.0, "USD") } returns true
// anyマッチャー
every { mockPaymentService.processPayment(any(), any()) } returns true
// 範囲マッチャー
every { mockPaymentService.processPayment(between(50.0, 150.0), any()) } returns true
// 文字列マッチャー
every { mockPaymentService.refund(match { it.startsWith("TXN-") }, any()) } returns true
// リストマッチャー
every { mockPaymentService.refund(ofType(String::class), any()) } returns true
assertTrue(mockPaymentService.processPayment(100.0, "USD"))
assertTrue(mockPaymentService.refund("TXN-12345", 50.0))
}
複数のマッチャーパターン
@Test
fun testAdvancedMatchers() {
val mockService = mockk<UserRepository>()
// any: あらゆる引数
every { mockService.save(any()) } returns true
// allAny: 複数引数のいずれでもマッチ
every { mockService.save(allAny()) } returns true
// eq: 等号チェック
every { mockService.save(eq(User(1, "John", "john@example.com"))) } returns true
// refEq: 参照による等号チェック
val user = User(1, "John", "john@example.com")
every { mockService.save(refEq(user)) } returns true
// neq: 不等号チェック
every { mockService.save(neq(User(0, "", ""))) } returns true
}
呼び出し回数の検証
@Test
fun testCallCounts() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.findById(any()) } returns User(1, "John", "john@example.com")
val userService = UserService(mockRepository)
// 1回だけ呼び出される
verify(exactly = 1) { mockRepository.findById(1) }
// 1回以上呼び出される
verify(atLeast = 1) { mockRepository.findById(any()) }
// 最大2回まで呼び出される
verify(atMost = 2) { mockRepository.findById(any()) }
// 呼び出されない
verify(exactly = 0) { mockRepository.deleteById(any()) }
// 呼び出されたことを確認
verify { mockRepository.findById(1) }
}
呼び出しの順序検証
@Test
fun testCallOrder() {
val mockRepository = mockk<UserRepository>()
val mockCache = mockk<CacheService>()
every { mockRepository.findById(any()) } returns User(1, "John", "john@example.com")
every { mockCache.get(any()) } returns null
every { mockCache.set(any(), any()) } just Runs
val userService = UserService(mockRepository, mockCache)
userService.getUser(1)
// 呼び出し順序を検証
verifyOrder {
mockCache.get("user_1")
mockRepository.findById(1)
mockCache.set("user_1", any())
}
// シーケンス検証
verifySequence {
mockCache.get("user_1")
mockRepository.findById(1)
mockCache.set("user_1", any())
}
}
スパイとPartialモック
スパイの基本
class RealUserRepository : UserRepository {
override fun findById(id: Int): User? {
return if (id > 0) User(id, "Real User", "real@example.com") else null
}
override fun save(user: User): Boolean = true
override fun deleteById(id: Int) { }
}
@Test
fun testSpyBasics() {
val realRepository = RealUserRepository()
val spyRepository = spyk(realRepository)
// 特定のメソッドのみをモック
every { spyRepository.save(any()) } returns false
// モックされたメソッド
val saved = spyRepository.save(User(1, "John", "john@example.com"))
assertFalse(saved)
// モックされていないメソッドは実装が実行
val user = spyRepository.findById(1)
assertNotNull(user)
assertEquals("Real User", user?.name)
}
Partialモック
@Test
fun testPartialMock() {
val mockRepository = mockk<UserRepository>(relaxed = true) {
every { findById(1) } returns User(1, "John", "john@example.com")
// 他のメソッドはrelaxedなので自動的にデフォルト値を返す
}
val result = mockRepository.findById(1)
assertEquals("John", result?.name)
// 未定義のメソッドはデフォルト値を返す
val saved = mockRepository.save(User(2, "Jane", "jane@example.com"))
assertNull(saved) // Boolean型のデフォルト値はfalse
}
キャプチャとスロット
引数のキャプチャ
@Test
fun testArgumentCapture() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.save(any()) } returns true
val userService = UserService(mockRepository)
userService.createUser("Alice", "alice@example.com")
// 呼び出しされた引数をキャプチャ
val slot = slot<User>()
verify { mockRepository.save(capture(slot)) }
val capturedUser = slot.captured
assertEquals("Alice", capturedUser.name)
assertEquals("alice@example.com", capturedUser.email)
}
複数の引数をキャプチャ
@Test
fun testMultipleArgumentCapture() {
val mockService = mockk<PaymentService>()
every { mockService.processPayment(any(), any()) } returns true
val paymentProcessor = PaymentProcessor(mockService)
paymentProcessor.charge(100.0, "USD")
val amountSlot = slot<Double>()
val currencySlot = slot<String>()
verify {
mockService.processPayment(
capture(amountSlot),
capture(currencySlot)
)
}
assertEquals(100.0, amountSlot.captured)
assertEquals("USD", currencySlot.captured)
}
スロットを使用した複数キャプチャ
@Test
fun testCapturingMultipleCalls() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.save(any()) } returns true
val userService = UserService(mockRepository)
userService.createUser("Alice", "alice@example.com")
userService.createUser("Bob", "bob@example.com")
val slots = mutableListOf<User>()
verify { mockRepository.save(capture(slots)) }
assertEquals(2, slots.size)
assertEquals("Alice", slots[0].name)
assertEquals("Bob", slots[1].name)
}
高度な機能
Coroutineのモック
interface UserServiceAsync {
suspend fun getUserAsync(id: Int): User?
suspend fun createUserAsync(user: User): Boolean
}
@Test
fun testCoroutineMocking() = runTest {
val mockService = mockk<UserServiceAsync>()
coEvery { mockService.getUserAsync(1) } returns User(1, "John", "john@example.com")
coEvery { mockService.createUserAsync(any()) } returns true
val result = mockService.getUserAsync(1)
assertEquals("John", result?.name)
val saved = mockService.createUserAsync(User(2, "Jane", "jane@example.com"))
assertTrue(saved)
coVerify { mockService.getUserAsync(1) }
coVerify { mockService.createUserAsync(any()) }
}
Flowのモック
interface StreamingService {
fun getUsersStream(): Flow<User>
}
@Test
fun testFlowMocking() = runTest {
val mockService = mockk<StreamingService>()
val users = listOf(
User(1, "Alice", "alice@example.com"),
User(2, "Bob", "bob@example.com"),
User(3, "Charlie", "charlie@example.com")
)
every { mockService.getUsersStream() } returns flowOf(*users.toTypedArray())
val result = mutableListOf<User>()
mockService.getUsersStream().collect { result.add(it) }
assertEquals(3, result.size)
verify { mockService.getUsersStream() }
}
ラムダ関数のモック
interface CallbackService {
fun executeWithCallback(callback: (String) -> Unit)
fun <T> executeWithResult(transformer: (String) -> T): T
}
@Test
fun testLambdaMocking() {
val mockService = mockk<CallbackService>()
val slot = slot<(String) -> Unit>()
every { mockService.executeWithCallback(capture(slot)) } answers {
slot.captured("result")
}
var callbackResult = ""
mockService.executeWithCallback { callbackResult = it }
assertEquals("result", callbackResult)
}
@Test
fun testGenericLambda() {
val mockService = mockk<CallbackService>()
every { mockService.executeWithResult(any<(String) -> Int>()) } returns 42
val result = mockService.executeWithResult { it.length }
assertEquals(42, result)
}
answersを使用した柔軟な応答
@Test
fun testAnswers() {
val mockRepository = mockk<UserRepository>()
// answersを使用して動的な応答を生成
every { mockRepository.save(any()) } answers {
val user = firstArg<User>()
println("Saving user: ${user.name}")
true
}
// invocationオブジェクトを使用してメタ情報にアクセス
every { mockRepository.findById(any()) } answers {
val id = firstArg<Int>()
User(id, "User_$id", "user$id@example.com")
}
mockRepository.save(User(1, "John", "john@example.com"))
val user = mockRepository.findById(5)
assertEquals("User_5", user.name)
}
returnsを使用したシーケンス応答
@Test
fun testReturnsSequence() {
val mockService = mockk<UserRepository>()
// consecutiveReturnsで複数の戻り値を指定
every { mockService.findById(any()) } returnsMany listOf(
User(1, "First", "first@example.com"),
User(2, "Second", "second@example.com"),
User(3, "Third", "third@example.com")
)
val first = mockService.findById(1)
val second = mockService.findById(2)
val third = mockService.findById(3)
assertEquals("First", first?.name)
assertEquals("Second", second?.name)
assertEquals("Third", third?.name)
}
拡張関数のモック
// 拡張関数の定義
fun UserRepository.findActiveUsers(): List<User> {
return listOf()
}
@Test
fun testExtensionFunctionMocking() {
val mockRepository = mockk<UserRepository>()
// 拡張関数をモック
every { mockRepository.findActiveUsers() } returns listOf(
User(1, "John", "john@example.com"),
User(2, "Jane", "jane@example.com")
)
val activeUsers = mockRepository.findActiveUsers()
assertEquals(2, activeUsers.size)
}
リフレクションを使用したプライベートメンバーへのアクセス
class SecureUserService(private val repository: UserRepository) {
private var cacheEnabled = false
fun enableCache() {
cacheEnabled = true
}
fun getUser(id: Int): User? {
return if (cacheEnabled) {
repository.findById(id)
} else {
repository.findById(id)
}
}
}
@Test
fun testPrivateMemberAccess() {
val mockRepository = mockk<UserRepository>()
every { mockRepository.findById(any()) } returns User(1, "John", "john@example.com")
val service = SecureUserService(mockRepository)
// プライベートメンバーにアクセス(リフレクション使用)
val field = SecureUserService::class.java.getDeclaredField("cacheEnabled")
field.isAccessible = true
field.setBoolean(service, true)
val result = service.getUser(1)
assertNotNull(result)
}
アーキテクチャと実装の詳細
MockKの内部アーキテクチャ
MockKの実装は、複数の層で構成されています:
1. Bytecode Manipulation層
MockKはバイトコード操作エンジンを使用して、実行時にクラスを動的に変換します。
┌─────────────────────────────┐
│ Kotlin Source Code │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ Kotlin Compiler │
│ (Bytecode Generation) │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ ByteBuddy/Javassist │
│ (Bytecode Instrumentation) │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ MockK Runtime │
│ (Mock Handling) │
└─────────────────────────────┘
2. モック作成フロー
// mockk<T>()の内部処理:
// 1. クラスTのバイトコードを取得
// 2. ByteBuddyまたはJavassistを使用して、全メソッドをインターセプト
// 3. インターセプトされたメソッドはMockKランタイムにデリゲート
// 4. 設定されたbehaviorに基づいて動作
class MockKImplementation<T> {
private val interceptors = mutableMapOf<String, MethodInterceptor>()
private val callRecorder = CallRecorder()
fun intercept(method: Method, args: Array<Any?>): Any? {
val methodSignature = method.signature()
return when {
methodSignature in interceptors -> {
callRecorder.recordCall(method, args)
interceptors[methodSignature]?.handle(args)
}
else -> {
callRecorder.recordCall(method, args)
null // デフォルト値
}
}
}
}
MockKが使用する主要な技術
ByteBuddy
// ByteBuddyを使用したバイトコード生成の概要
class ByteBuddyMockEngineImpl : MockingEngine {
override fun createMock(
type: KClass<*>,
baseInstance: Any?,
name: String,
relaxed: Boolean
): Any {
val enhancedClass = ByteBuddy()
.subclass(type.java)
.method(ElementMatchers.any())
.intercept(MethodDelegation.to(MockInvocationHandler))
.make()
.load(type.java.classLoader)
.loaded
return enhancedClass.getDeclaredConstructor().newInstance()
}
}
Javassist(代替エンジン)
// Javassistを使用した動的クラス生成
class JavassistMockEngine : MockingEngine {
override fun createMock(
type: KClass<*>,
baseInstance: Any?,
name: String,
relaxed: Boolean
): Any {
val classPool = ClassPool.getDefault()
val sourceClass = classPool.get(type.qualifiedName)
val enhancedClass = classPool.makeClass("MockOf_${type.simpleName}")
// すべてのメソッドにインターセプターを追加
sourceClass.declaredMethods.forEach { method ->
addInterceptor(enhancedClass, method)
}
return enhancedClass.toClass().newInstance()
}
}
呼び出し記録メカニズム
class CallRecorder {
private val recordedCalls = mutableListOf<RecordedCall>()
private val callstack = mutableListOf<Call>()
fun recordCall(
method: Method,
arguments: Array<Any?>,
result: Any? = null,
throwable: Throwable? = null
) {
val call = Call(
method = method,
args = arguments,
timestamp = System.nanoTime(),
result = result,
throwable = throwable
)
recordedCalls.add(RecordedCall(call))
callstack.add(call)
}
fun getAllCalls(): List<RecordedCall> = recordedCalls.toList()
fun getLastCall(): RecordedCall? = recordedCalls.lastOrNull()
fun getCallsByMethod(method: Method): List<RecordedCall> {
return recordedCalls.filter { it.call.method == method }
}
}
マッチャー実装
MockKのマッチャーシステムは、引数の柔軟なマッチングを可能にします:
// マッチャーの基本インターフェース
abstract class Matcher<T> {
abstract fun match(arg: T?): Boolean
abstract fun toString(): String
}
// 具体的なマッチャー実装
class AnyMatcher<T> : Matcher<T>() {
override fun match(arg: T?): Boolean = true
override fun toString() = "any()"
}
class EqMatcher<T>(private val value: T) : Matcher<T>() {
override fun match(arg: T?): Boolean = arg == value
override fun toString() = "eq($value)"
}
class BetweenMatcher<T : Comparable<T>>(
private val from: T,
private val to: T
) : Matcher<T>() {
override fun match(arg: T?): Boolean = arg != null && arg >= from && arg <= to
override fun toString() = "between($from, $to)"
}
class RegexMatcher(private val pattern: Regex) : Matcher<String>() {
override fun match(arg: String?): Boolean = arg != null && pattern.containsMatchIn(arg)
override fun toString() = "matches($pattern)"
}
// ジェネリック型マッチャー
class OfTypeMatcher<T>(private val klass: KClass<T>) : Matcher<Any>() {
override fun match(arg: Any?): Boolean = arg != null && klass.isInstance(arg)
override fun toString() = "ofType(${klass.simpleName})"
}
// Lambda ベースのマッチャー
class LambdaMatcher<T>(private val predicate: (T?) -> Boolean) : Matcher<T>() {
override fun match(arg: T?): Boolean = predicate(arg)
override fun toString() = "matches(predicate)"
}
// マッチャーの使用例
@Test
fun testMatcherUsage() {
val mockRepository = mockk<UserRepository>()
// anyマッチャーで任意の引数を受け入れ
every { mockRepository.save(any()) } returns true
// eqマッチャーで正確な値をマッチ
val expectedUser = User(1, "John", "john@example.com")
every { mockRepository.save(eq(expectedUser)) } returns true
// betweenマッチャーで範囲をマッチ
every { mockRepository.findById(between(1, 100)) } returns User(1, "John", "john@example.com")
// カスタムラムダマッチャー
every {
mockRepository.save(match { user -> user.email.contains("@example.com") })
} returns true
}
スパイのプロキシ実装
class SpyProxy<T>(
private val instance: T,
private val interceptors: Map<String, MethodInterceptor> = emptyMap()
) : InvocationHandler {
private val callRecorder = CallRecorder()
override fun invoke(
proxy: Any,
method: Method,
args: Array<Any?>?
): Any? {
val methodSignature = method.signature()
val arguments = args ?: emptyArray()
return try {
when {
methodSignature in interceptors -> {
callRecorder.recordCall(method, arguments)
interceptors[methodSignature]?.handle(arguments)
}
else -> {
// 元のインスタンスのメソッドを呼び出し
callRecorder.recordCall(method, arguments)
val result = method.invoke(instance, *arguments)
callRecorder.recordCall(method, arguments, result)
result
}
}
} catch (e: Exception) {
callRecorder.recordCall(method, arguments, throwable = e)
throw e
}
}
}
relaxedモックの実装
class RelaxedMockK<T>(
private val mockEngine: MockingEngine
) : MockK<T> {
private val autoAnswers = mapOf(
Boolean::class to false,
Byte::class to 0.toByte(),
Char::class to '\u0000',
Short::class to 0.toShort(),
Int::class to 0,
Long::class to 0L,
Float::class to 0.0f,
Double::class to 0.0,
String::class to "",
List::class to emptyList<Any>(),
Map::class to emptyMap<Any, Any>(),
Set::class to emptySet<Any>()
)
fun createRelaxedMock(type: KClass<*>): Any {
val mockInstance = mockEngine.createMock(type)
// すべてのメソッドに自動応答を設定
type.java.methods.forEach { method ->
val returnType = method.returnType.kotlin
val defaultValue = autoAnswers[returnType]
if (defaultValue != null) {
// デフォルト値を返すように設定
setDefaultAnswer(mockInstance, method, defaultValue)
} else if (returnType.isData) {
// データクラスの場合は空のインスタンスを作成
val emptyInstance = createEmptyDataClass(returnType)
setDefaultAnswer(mockInstance, method, emptyInstance)
}
}
return mockInstance
}
private fun createEmptyDataClass(klass: KClass<*>): Any {
// プライマリコンストラクタのパラメータを取得
val constructor = klass.primaryConstructor
?: throw IllegalArgumentException("Cannot create empty instance of $klass")
// すべてのパラメータにデフォルト値を使用
val params = constructor.parameters.map { param ->
param to getDefaultValueForType(param.type)
}.toMap()
return constructor.callBy(params)
}
private fun getDefaultValueForType(type: KType): Any? {
return when {
type.classifier == String::class -> ""
type.classifier == Int::class -> 0
type.classifier == Boolean::class -> false
type.classifier == Double::class -> 0.0
type.isMarkedNullable -> null
else -> null
}
}
}
verify関数の実装
fun <T> verify(
inverse: Boolean = false,
exactly: Int? = null,
atLeast: Int? = null,
atMost: Int? = null,
block: T.() -> Unit
) {
val callRecorder = getCurrentCallRecorder()
val recordedCalls = callRecorder.getAllCalls()
val calls = recordedCalls.filter { call ->
// ブロック内での呼び出しをフィルタリング
}
val actualCount = calls.size
val expectedCount = when {
exactly != null -> exactly
atLeast != null && atMost != null -> {
if (actualCount < atLeast || actualCount > atMost) {
throw AssertionError(
"Expected between $atLeast and $atMost calls, but got $actualCount"
)
}
return
}
atLeast != null -> {
if (actualCount < atLeast) {
throw AssertionError(
"Expected at least $atLeast calls, but got $actualCount"
)
}
return
}
atMost != null -> {
if (actualCount > atMost) {
throw AssertionError(
"Expected at most $atMost calls, but got $actualCount"
)
}
return
}
else -> 1
}
if (inverse) {
if (actualCount > 0) {
throw AssertionError("Expected 0 calls, but got $actualCount")
}
} else {
if (actualCount != expectedCount) {
throw AssertionError(
"Expected $expectedCount calls, but got $actualCount"
)
}
}
}
ベストプラクティス
1. テスト構造の確立
class UserServiceTest {
private lateinit var mockRepository: UserRepository
private lateinit var userService: UserService
@BeforeEach
fun setUp() {
mockRepository = mockk()
userService = UserService(mockRepository)
}
@Test
fun testGetUserSuccess() {
// Arrange: テストデータとモックの設定
val userId = 1
val expectedUser = User(userId, "John Doe", "john@example.com")
every { mockRepository.findById(userId) } returns expectedUser
// Act: テスト対象のメソッドを実行
val result = userService.getUser(userId)
// Assert: 結果を検証
assertNotNull(result)
assertEquals(expectedUser, result)
verify { mockRepository.findById(userId) }
}
}
2. ファクトリーメソッドの使用
object MockFactory {
fun createMockUserRepository(
findByIdReturn: User? = null,
saveReturn: Boolean = true
): UserRepository = mockk {
every { findById(any()) } returns findByIdReturn
every { save(any()) } returns saveReturn
every { deleteById(any()) } just Runs
}
fun createMockUserService(
repository: UserRepository = createMockUserRepository()
): UserService = UserService(repository)
}
@Test
fun testWithFactory() {
val mockRepository = MockFactory.createMockUserRepository(
findByIdReturn = User(1, "John", "john@example.com")
)
val result = mockRepository.findById(1)
assertNotNull(result)
}
3. テストダブルの作成
// Dummy: パラメータとして渡されるだけで使用されない
class DummyUserRepository : UserRepository {
override fun findById(id: Int): User? = null
override fun save(user: User): Boolean = false
override fun deleteById(id: Int) {}
}
// Stub: 決まった戻り値を返す
class StubUserRepository : UserRepository {
override fun findById(id: Int): User? = User(1, "John", "john@example.com")
override fun save(user: User): Boolean = true
override fun deleteById(id: Int) {}
}
// Mock: 期待値を事前に指定
fun createMockUserRepository(): UserRepository = mockk {
every { findById(any()) } returns User(1, "John", "john@example.com")
every { save(any()) } returns true
}
4. 複雑なモック構造の管理
class ComplexServiceTest {
private lateinit var mockUserRepository: UserRepository
private lateinit var mockPaymentService: PaymentService
private lateinit var mockNotificationService: NotificationService
private lateinit var userManagementService: UserManagementService
@BeforeEach
fun setUp() {
mockUserRepository = mockk()
mockPaymentService = mockk()
mockNotificationService = mockk()
userManagementService = UserManagementService(
userRepository = mockUserRepository,
paymentService = mockPaymentService,
notificationService = mockNotificationService
)
}
@Test
fun testCompleteUserCreationFlow() {
// Arrange
val newUser = User(0, "Alice", "alice@example.com")
val savedUser = newUser.copy(id = 1)
every { mockUserRepository.save(any()) } returns true
every { mockUserRepository.findById(1) } returns savedUser
every { mockPaymentService.createAccount(any()) } returns true
every { mockNotificationService.sendWelcomeEmail(any()) } just Runs
// Act
val result = userManagementService.createNewUser(newUser)
// Assert
assertTrue(result)
verifySequence {
mockUserRepository.save(newUser)
mockPaymentService.createAccount(savedUser)
mockNotificationService.sendWelcomeEmail(savedUser)
}
}
}
トラブルシューティング
一般的な問題と解決策
1. "Cannot find default argument for method"エラー
// 問題
interface Service {
fun process(id: Int, flag: Boolean = true)
}
@Test
fun testWithDefaultArgument() {
val mockService = mockk<Service>()
every { mockService.process(any()) } just Runs // エラー
}
// 解決策
@Test
fun testWithDefaultArgumentFixed() {
val mockService = mockk<Service>()
every { mockService.process(any(), any()) } just Runs // 正しい
}
2. "Matcher mismatch"エラー
// 解決策1: any()を使用
@Test
fun testMatcherMismatchFixed() {
val mockService = mockk<UserRepository>()
every { mockService.save(any()) } returns true
val user = User(1, "John", "john@example.com")
mockService.save(user)
verify { mockService.save(any()) }
}
3. Coroutineモックの問題
// 解決策
@Test
fun testAsyncServiceCorrect() = runTest {
val mockService = mockk<AsyncService>()
coEvery { mockService.getData() } returns "data"
val result = mockService.getData()
assertEquals("data", result)
coVerify { mockService.getData() }
}
4. デバッグ技法
@Test
fun testWithDebugOutput() {
val mockService = mockk<UserRepository>(name = "MyMockRepository")
every { mockService.findById(any()) } answers {
println("findById called with ${firstArg<Int>()}")
User(firstArg(), "Debug User", "debug@example.com")
}
val result = mockService.findById(1)
// すべての呼び出しを表示
println("All calls: ${mockService._calls}")
}
結論
MockKは、Kotlin開発におけるユニットテストの実装を大幅に簡素化・効率化するためのツールです。Kotlin特有の言語機能(データクラス、拡張関数、Coroutineなど)への完全なサポートにより、テスト自体も簡潔で読みやすくなります。
このガイドで説明した基本的な使用方法から高度な機能、アーキテクチャの詳細までを理解することで、開発者は堅牢で保守性の高いテストスイートを構築できるようになります。特に、Arrange-Act-Assertパターンの遵守とベストプラクティスの適用により、テストコードの品質と効率が向上し、最終的にはプロダクトコードの品質向上につながります。
MockKの活用により、Kotlin開発はさらに効率的で堅牢なものになるでしょう。