mcpField
M
mocking-with-mockk

mocking-with-mockk

github.com/skydovesテスト・品質33979 回インストール22 週間前に更新
リポジトリ

mocking-with-mockk について

Kotlin テストで MockK を使うためのガイドです。コルーチンや Kotlin 固有の機能をモックしたい開発者向けに、suspend 関数のスタブと検証、オブジェクトや静的モック、引数の捕捉、グローバルモックの後処理を説明します。

主な機能

  1. 01suspend 関数のスタブと検証
  2. 02オブジェクト、静的関数、コンストラクターのモック
  3. 03呼び出し回数と順序の検証
  4. 04呼び出し引数の捕捉
  5. 05厳格モックと緩和モックの選択

活用シーン

  1. 01コルーチンを使う Kotlin コードのテスト
  2. 02シングルトンやトップレベル関数のモック
  3. 03依存先の呼び出しと順序の確認
  4. 04渡された引数の捕捉と検証

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/skydoves/android-testing-skills --skill mocking-with-mockk

インストール後、Claude Code で/mocking-with-mockkを入力して読み込まれたか確認します。

詳細

MockK によるモック作成 — Kotlin ファーストのモック用スタック

MockK は Mockito に代わる Kotlin ファーストの選択肢です。主な強みは、サスペンド関数をネイティブにサポートしていること(runBlocking を使わずに coEvery / coVerify を利用できます)、シングルトン/object/静的メソッド/コンストラクターのモック作成を第一級でサポートしていること、Kotlin ネイティブの構文(every { … } returns x)です。一方、androidx 自体は MockK を使用していません(AOSP のチェックアウトに対して grep -r "io.mockk" を実行してもヒットは 0 件です。主流のパターンについては ../mocking-with-mockito/SKILL.md を参照してください)。Google 以外の新しい Kotlin コードでは MockK が選ばれることが多く、Google 内の新しい Kotlin コードや AOSP の規約に合わせる場合は Mockito が選ばれます。どちらも有効な選択肢です。

このスキルを使う場合

  • コードベースが Kotlin のみで、サスペンド関数/コルーチンが中心であり、ユーザーがサスペンド関数のスタブを簡潔に記述したい場合。
  • ユーザーが Kotlin の object(シングルトン)、コンパニオンオブジェクト、トップレベル関数(Kt が末尾に付くファイル)、またはクラスの新規インスタンスすべてをモックしたい場合。
  • ユーザーが、デフォルトでは厳密に検証しつつ、モックごとに relaxed = true / relaxUnitFun = true を指定して無効にできるようにしたい場合。
  • ユーザーが slot<T>() と slot.captured によるキャプチャを使いたい場合(Mockito の ArgumentCaptor より簡潔です)。
  • ユーザーが viewModelScope、Flow のコレクター、LaunchedEffect スタイルのサスペンド関数を使うコラボレーターのテストを作成している場合。

このスキルを使わない場合

  • コードベースが Java と Kotlin の混在、またはすでに Mockito に標準化されている場合。../mocking-with-mockito/SKILL.md を使ってください。理由なく移行しないでください。どちらのフレームワークも有効です。
  • AOSP/androidx の規約に合わせる場合(Mockito のみが使われています)。../mocking-with-mockito/SKILL.md を使ってください。
  • インタラクションより振る舞いが重要な場合(たとえば、キャッシュロジックを持つ Repository)。代わりにフェイクを作成してください。../../../fundamentals/doubles/picking-test-doubles/SKILL.md を参照してください(/test-doubles では Google がフェイクを明示的に推奨しています)。
  • テストランナー/Gradle のマトリックスがまだ設定されていない場合。まず ../../runner/configuring-junit4-on-android/SKILL.md を参照してください。
  • 時間の経過に伴う Flow のエミッションをテストする場合。MockK と Turbine を併用してください。../../coroutines/testing-flows-with-turbine/SKILL.md を参照してください。
  • runTest のセマンティクスが必要な場合。../../coroutines/testing-coroutines-with-runtest/SKILL.md を参照してください。

前提条件

  • ../../runner/configuring-junit4-on-android/SKILL.md にある基本的なテスト配線がすでに設定されていること。
  • テスト対象コードが Dispatchers.Main に触れる場合は、MainDispatcherRule が導入されていること。テストランナーのスキルを参照してください。
  • MockK をインストルメンテーションで使う場合(Android ランタイム)、デバイスは API 21 以降であること(MockK は dexmaker 上で API 21 までサポートしています)。

ワークフロー

  • 1. MockK の依存関係を追加します。 特定の 1.14.x バージョン(執筆時点での最新)に固定します。
dependencies {
    // JVM unit tests (src/test/)
    testImplementation("io.mockk:mockk-jvm:1.14.0")

    // Instrumented tests (src/androidTest/) — different artifact + agent
    androidTestImplementation("io.mockk:mockk-android:1.14.0")
    androidTestImplementation("io.mockk:mockk-agent:1.14.0")
}

mockk-android はバイトコード生成バックエンドを切り替えるため、標準の ByteBuddy が動作しない Dalvik/ART 上でも MockK を実行できます。src/test/ の Robolectric テストでは mockk-android ではなく mockk-jvm を使用してください。Robolectric は JVM 上で動作します。

  • 2. MockKAnnotations.init(this) または mockk<T>() の直接生成でモックを設定します。 多くのモックを使うテストではアノテーションを使う方法がすっきりします。単発のモックでは直接生成が適しています。
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import io.mockk.impl.annotations.InjectMockKs

@RunWith(AndroidJUnit4::class)
class UserViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @MockK lateinit var repo: UserRepository
    @RelaxedMockK lateinit var logger: Logger
    @SpyK var realClock: Clock = SystemClock()
    @InjectMockKs lateinit var subject: UserViewModel

    @Before fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true)
    @After fun tearDown() = unmockkAll()   // see step 5 — cleanup global hooks
}

MockKAnnotations.init は、グローバルフック(mockkStatic/mockkObject/mockkConstructor)をそれ自体で設定することはありません。ただし、テストクラスでこれらを追加する場合は、兄弟テストに影響が漏れないよう @After unmockkAll() が必要です。JUnit5 の MockKExtension はこれを自動で行いますが、JUnit4 では行いません。

直接生成する場合:

val car = mockk<Car>(
    name = "carA",
    relaxed = false,
    relaxUnitFun = true,           // Unit-returning calls auto-relax; reference returns still throw
    moreInterfaces = arrayOf(Comparable::class),
)
  • 3. 通常の関数には every { … } returns … を、suspend 関数には coEvery { … } returns … を使ってスタブを設定します。 Mockito と比べて特に使いやすい点は、suspend 関数のスタブ設定で runBlocking { whenever(...) } のラッパーが不要なことです。
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.verify

every { car.drive(Direction.NORTH) } returns Outcome.OK
every { car.brake() } throws IllegalStateException("frozen")
every { car.gear } returnsMany listOf(1, 2, 3)              // sequence
every { car.compute(any()) } answers { firstArg<Int>() * 2 }

// Suspend — the big win:
coEvery { repo.fetchUser(1) } returns User("Jane")
coEvery { repo.observeUser(1) } returns flowOf(User("Jane"))
  • 4. verify { … }、coVerify { … }、および厳密なバリアントを使ってインタラクションを検証します。 MockK の検証機能は Mockito よりも充実しています。
verify { car.drive(Direction.NORTH) }
verify(exactly = 2) { car.drive(any()) }
verify(exactly = 0) { car.brake() }                          // = never
verify(atLeast = 1, atMost = 3) { car.drive(any()) }

verifyAll { /* must list every interaction — exhaustive, unordered */ }
verifySequence { /* every interaction in this exact order */ }
verifyOrder { /* listed interactions occurred in this order, others may also have happened */ }

coVerify(exactly = 1) { repo.fetchUser(1) }

confirmVerified(car)                                         // strict: nothing else was invoked

confirmVerified は、気付かないまま発生した余分なインタラクションを検出するための安全策です。完全に制約されたテストにするには、verifyAll/verifySequence と併用します。

  • 5. slot<T>() で引数をキャプチャします。 Mockito のキャプチャーよりも簡潔で、slot.captured はプロパティです。
import io.mockk.slot

val slot = slot<User>()
every { repo.save(capture(slot)) } returns Unit

subject.register(User("Jane"))

assertEquals("Jane", slot.captured.name)

// Multiple captures over a sequence of calls:
val users = mutableListOf<User>()
every { repo.save(capture(users)) } returns Unit
// ... many calls ...
assertEquals(3, users.size)
  • 6. relaxed = true または relaxUnitFun = true を意図的に使用します。 MockK はデフォルトで 厳密 です。スタブされていない呼び出しはすべて MockKException をスローします。2 つの緩和モードは次のとおりです。

    • relaxed = true — スタブされていないすべての呼び出しで、型のデフォルト値(0、false、空文字列、参照型ではネストされたモック)を返します。定型コードを減らせますが、バグを見逃しやすくなります。
    • relaxUnitFun = true — Unit を返す呼び出しのみ自動的に緩和します。参照型を返す呼び出しは引き続き例外をスローします。ロガー/メトリクス/フレームワークのコールバックと連携する場合に推奨される中間的な選択肢です。
val logger = mockk<Logger>(relaxed = true)             // any call -> default
logger.info("anything")                                // no every{} needed
val n = logger.lineCount                               // returns 0

val analytics = mockk<Analytics>(relaxUnitFun = true)  // safer middle ground
analytics.track(Event("foo"))                          // OK — Unit return is relaxed
analytics.session                                      // throws — reference return needs every{}
  • 7. mockkObject/mockkStatic/mockkConstructor でシングルトン/静的メソッド/コンストラクターをモックし、必ずクリーンアップします。 これらはグローバルフックを設定するため、明示的に解除しない限り、現在のテストが終わった後も残ります。テスト間への漏れは、MockK で最もよくある落とし穴です。
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.mockkConstructor
import io.mockk.unmockkAll

@After fun tearDown() = unmockkAll()    // belt-and-braces cleanup

@Test fun mocksObject() {
    mockkObject(MySingleton)
    every { MySingleton.flag } returns true
    // ... assertions ...
}

@Test fun mocksTopLevel() {
    // Kotlin top-level functions compile as static methods on the file's *Kt class.
    mockkStatic("com.example.UtilsKt")
    every { hashSomething(any()) } returns "deadbeef"
}

@Test fun mocksJavaStatic() {
    mockkStatic(System::class)
    every { System.currentTimeMillis() } returns 0L
}

@Test fun mocksConstructor() {
    mockkConstructor(OkHttpClient::class)
    every { anyConstructed<OkHttpClient>().newCall(any()) } returns fakeCall
}

JUnit5 の MockKExtension は自動でクリーンアップしますが、JUnit4 のテストでは @After で unmockkAll() を呼び出す必要があります。

  • 8. テストが実行でき、厳密モードでの失敗が実際に発生することを確認します。 ./gradlew :<module>:testDebugUnitTest --tests <YourTestClass> を実行します。厳密なモックに対して verifyAll/verifySequence/confirmVerified を使用したテストが成功する場合、すべてのインタラクションが検証されています。MockK が MockKException: no answer found for: … をスローする場合、SUT がテストでスタブしていない何かを呼び出しています。スタブを追加するか、モックを緩和するか、SUT を修正してください。

パターン

パターン:間違った方法と正しい方法 — suspend 関数のスタブ設定
// WRONG
every { mock.suspendFn() } returns x
// WRONG because: every { } produces a non-suspend stub. At call time MockK throws
// MockKException because the actual call carries a Continuation argument that
// every {} did not match against. This is the #1 trip-up when migrating from Mockito.
// RIGHT
coEvery { mock.suspendFn() } returns x
coVerify { mock.suspendFn() }
パターン:間違った方法と正しい方法 — テスト間で静的メソッド/オブジェクトのモックが残る場合
// WRONG
@Test fun firstTest() {
    mockkStatic(System::class)
    every { System.currentTimeMillis() } returns 0L
    // ... assertions ...
    // No unmockkStatic, no unmockkAll, no JUnit5 extension.
}

@Test fun secondTest() {
    // System.currentTimeMillis() is STILL mocked from firstTest. Bizarre failures
    // depending on test ordering follow.
}
// WRONG because: mockkStatic / mockkObject / mockkConstructor install global hooks.
// JUnit4 does not auto-clean MockK state.
// RIGHT — JUnit4 with explicit teardown
@After fun tearDown() = unmockkAll()

@Test fun firstTest() {
    mockkStatic(System::class)
    every { System.currentTimeMillis() } returns 0L
    // ...
}
// secondTest now starts with a clean slate.
パターン:間違った方法と正しい方法 — 緩和モードの選択
// WRONG
val service = mockk<PaymentService>(relaxed = true)
service.charge(amount = 100, account = "acct-7")
// Test passes because charge() silently returns a relaxed default. No verify call,
// no every{} stub. The intended interaction was never asserted, and a behaviour
// regression would not surface here.
// RIGHT
val service = mockk<PaymentService>()                // strict
every { service.charge(any(), any()) } returns ChargeResult.OK

service.charge(amount = 100, account = "acct-7")

verify(exactly = 1) { service.charge(eq(100), eq("acct-7")) }
confirmVerified(service)

テストで戻り値を本当に気にしないロガー/メトリクスには、relaxed = true が適しています。ドメインサービスでは、デフォルトの厳密モードを使い、明示的に every/verify を指定することをおすすめします。

必須ルール

  • 必ず suspend 関数のスタブには coEvery { … } returns …(every { … } returns … ではありません)を使用してください。suspend 呼び出しの検証には、必ず coVerify { … }(verify { … } ではありません)を使用してください。
  • src/test/ では io.mockk:mockk-jvm に、src/androidTest/ では io.mockk:mockk-android と io.mockk:mockk-agent に必ず依存してください。これらのアーティファクトを混在させると、エージェントがクラッシュします。
  • JUnit4 テストで mockkObject、mockkStatic、または mockkConstructor を使用する場合は、@After メソッドで必ず unmockkAll() を呼び出してください。テスト間のモックの漏洩は、MockK で最もよくある落とし穴です。
  • 判断に迷った場合は、relaxed = true よりも relaxUnitFun = true を優先してください。こちらのほうが隠してしまうバグが大幅に少なくなります。
  • @MockK / @RelaxedMockK / @SpyK / @InjectMockKs を使用する場合は、@Before で必ず MockKAnnotations.init(this, relaxUnitFun = …) を呼び出してください。これを行わないと、lateinit フィールドは初期化されません。
  • プロジェクトが JUnit 5 を使用している場合は、手動クリーンアップよりも JUnit5 の MockKExtension を必ず優先してください。静的モック/オブジェクトモックの後処理が自動的に行われます。
  • コードベースのほかの部分で採用されているモックの選択に必ず合わせてください。チームの合意なしに、Mockito を標準としているモジュールに MockK を導入したり、その逆を行ったりしないでください。
  • 動作が複雑な協力オブジェクトには、必ずフェイクを優先してください(developer.android.com/training/testing/fundamentals/test-doubles の「フェイクが優先されます」を参照)。MockK は動作の再実装ではなく、インタラクションの検証に使用してください。
  • inline 関数や inline class(値クラス)はモックしないでください。MockK ではインターセプトできません。代わりにラップするか、リファクタリングしてください。
  • 本番コードの時刻取得(System.currentTimeMillis、Clock.system)に mockkStatic を使用しないでください。代わりに Clock 抽象化を注入してください。バイトコードを書き換えなくても、本番コードはテスト可能です。
  • 推奨: verifySequence(または verifyAll)と confirmVerified(mock) を組み合わせ、予期しないインタラクションがあると失敗する、制約の完全なテストにしてください。

検証

  • ./gradlew :<module>:testDebugUnitTest --tests <YourTestClass> が成功すること。
  • every { mock.<suspend-fn>() } returns ... は実行時に MockKException("no answer found") を発生させます。grep ではなく、テストの失敗でこれを検出してください。(よくある suspend 関数名の断片を grep すると誤検出が多くなります。loadConfig、fetchSync、observeForever は、通常 suspend 関数ではありません。)
  • mockkStatic / mockkObject / mockkConstructor を使用しているすべてのテストファイルに、@After fun tearDown() = unmockkAll() があるか、MockKExtension(JUnit5)を拡張していること。
  • io.mockk.* と org.mockito.* の両方をインポートしているファイルがないこと。モジュールごとにフレームワークを1つ選んでください。
  • @MockK / @RelaxedMockK / @SpyK / @InjectMockKs フィールドは、MockKAnnotations.init(this, ...) を呼び出す @Before メソッドと組み合わせること。
  • テスト内の本番コードパスで、時刻取得に mockkStatic(System::class) を使用していないこと。代わりに Clock 抽象化を注入してください。
  • 厳格モードの失敗が実際に発生すること。スタブを削除すると、SUT の実行時に MockKException: no answer found for: … が発生すること。

参考資料

コメント