mcpField
M
mocking-with-mockito

mocking-with-mockito

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

mocking-with-mockito について

Kotlin Android テストで Mockito と mockito-kotlin を使うためのスキルガイドです。テストを書く Android 開発者向けに、依存関係、モックのライフサイクル、Kotlin DSL を説明し、final クラスのモックや引数マッチャー、コルーチンテストの注意点も扱います。

主な機能

  1. 01Mockito と mockito-kotlin の設定
  2. 02モックのライフサイクル方式の選択
  3. 03Kotlin DSL によるモック作成と引数取得
  4. 04引数マッチャーの適切な組み合わせ
  5. 05final クラスのモック設定
  6. 06厳格なスタブ検証の有効化

活用シーン

  1. 01Kotlin Android のユニットテストでモックを作成
  2. 02final クラスをモックできない問題の解決
  3. 03マッチャーエラーや未使用スタブの調査

インストール

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

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

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

詳細

Mockito によるモック — androidx ネイティブのモックスタック

Mockito は、androidx 自身が使用しているモックフレームワークです。AOSP のチェックアウト全体で 400 以上のテストファイルが org.mockito をインポートしている一方、io.mockk のヒット数はゼロです。「Google と同じやり方にしたい」新しい Android コードでは、Mockito と mockito-kotlin の Kotlin DSL が選ばれます。このスキルでは、正確な依存関係の組み合わせ、3 種類のセットアップ方法、Kotlin DSL の機能、Kotlin の final クラスに必要なインラインモックメーカーの設定を説明します。

このスキルを使用する場面

  • ユーザーが Kotlin Android モジュールで新しいテストスイートを始めようとしていて、「どのモックライブラリを使えばよいか」と尋ねている場合。
  • ユーザーが Mockito 4.x で Cannot mock/spy class … final class と報告し、mock-maker-inline の設定が必要な場合。
  • ユーザーが any()/eq() と生の値を混在させて、InvalidUseOfMatchersException と報告している場合。
  • ユーザーが Kotlin の when キーワードとの衝突(`when`(mock.foo()))に悩んでいて、whenever DSL を使いたい場合。
  • ユーザーが ArgumentCaptor.forClass(...) の代わりに、firstValue/lastValue/allValues を使った argumentCaptor<T>() を使いたい場合。
  • ユーザーが MockitoAnnotations.initMocks(this) から openMocks(this) または MockitoJUnit.rule() の方法に移行しようとしている場合。
  • ユーザーが API < 28 のインストルメンテッドテストで final クラスをモックしようとしている場合。

このスキルを使用しない場面

  • ユーザーが suspend 関数用の coEvery/coVerify、mockkObject によるシングルトンの深いモック、または mockkStatic による静的モックを必要としている場合は、../mocking-with-mockk/SKILL.md を使用してください。
  • ユーザーが設計レベルでフェイクとモックのどちらを使うか選ぼうとしている場合は、../../../fundamentals/doubles/picking-test-doubles/SKILL.md を使用してください。/test-doubles では、振る舞いが複雑な協力オブジェクトにはモックよりフェイクが推奨されています。
  • テストランナー/Gradle の組み合わせがまだ設定されていない場合は、../../runner/configuring-junit4-on-android/SKILL.md から始めてください。
  • ユーザーがコルーチンの runTest に対応したテストを必要としている場合は、../../coroutines/testing-coroutines-with-runtest/SKILL.md を参照してください。

前提条件

  • ../../runner/configuring-junit4-on-android/SKILL.md にある基本的なテスト設定が済んでいること(@RunWith(AndroidJUnit4::class)、androidx.test.ext:junit:1.3.0、JUnit 4.13.2)。
  • JDK 11 以降を使用する Kotlin モジュールであること(Mockito 5.x では JDK の最低要件が Java 11 に引き上げられました)。
  • テスト対象のコードが Dispatchers.Main に触れる場合は、MainDispatcherRule が設定されていること。runner スキルを参照してください。

ワークフロー

  • 1. Mockito と mockito-kotlin の依存関係を追加します。 インラインモックメーカーがデフォルトで有効になっているため、Kotlin の final クラスに対してプラグインファイルも mockito-inline への差し替えも不要な Mockito 5.x を、推奨する基本バージョンとしてください。
dependencies {
    testImplementation("org.mockito:mockito-core:5.14.2")
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")

    // Instrumented final-class mocks (API 28+ only — see step 7)
    androidTestImplementation("org.mockito:mockito-android:5.14.2")
    androidTestImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
}

mockito-kotlin はフォークではありません。mockito-core に直接処理を委譲する、薄い DSL ヘルパーレイヤーです。どちらもクラスパスに含める必要があります。

  • 2. モックのライフサイクル方式を選びます。 慣用的な選択肢は 3 つあります。新しいコードではルールを優先し、テストクラスで @RunWith を使用できない場合に openMocks を使ってください。
// (a) JUnit4 rule — preferred. No runner ownership, plays nicely with @RunWith(AndroidJUnit4::class).
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()

// (b) Manual openMocks — works everywhere.
private lateinit var closeable: AutoCloseable
@Before fun setUp() { closeable = MockitoAnnotations.openMocks(this) }
@After  fun tearDown() { closeable.close() }

// (c) JUnit5 extension (only if the project is on JUnit 5 via the Vintage engine).
@ExtendWith(MockitoExtension::class)

非推奨の MockitoAnnotations.initMocks(this) は void を返し、インラインモックのリソースをリークさせます。必ず openMocks(this) を使い、返された AutoCloseable を @After で閉じてください。

  • 3. モックを宣言し、mockito-kotlin DSL でスタブを設定します。 Kotlin 向けのエントリーポイントは org.mockito.kotlin.* にあります。
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class UserViewModelTest {
    @get:Rule val mockitoRule = MockitoJUnit.rule()
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    private val repo: UserRepository = mock()
    private val analytics: Analytics = mock {
        on { isEnabled() } doReturn true
    }

    @Test fun loadsUserAndLogs() = runTest {
        whenever(repo.findById(7L)) doReturn User("Jane")

        val vm = UserViewModel(repo, analytics)
        vm.load(7L)
        advanceUntilIdle()

        val captor = argumentCaptor<AnalyticsEvent>()
        verify(analytics, atLeastOnce()).log(captor.capture())
        assertThat(captor.lastValue.name).isEqualTo("user_loaded")
        verify(repo).findById(eq(7L))
    }
}

要点:

  • mock<T>() は reified なので、T::class.java を指定する必要はありません。
  • mock { on { … } doReturn … } はビルダーブロックです。生成時にスタブを設定する場合、whenever の呼び出しを連ねずに済みます。
  • whenever(...) は、Kotlin のキーワード when と衝突するためバッククォートが必要な、生の Mockito の `when`(...) の代わりになります。
  • argumentCaptor<T>() は、firstValue/secondValue/lastValue/allValues プロパティを持つキャプターを返します。getValue() を呼び出す必要はありません。
  • org.mockito.kotlin.* の any()、anyOrNull()、eq() は Kotlin で null セーフです(生の Mockito の any() は Java の null を返すため、Kotlin の null 非許容パラメーターでクラッシュします)。
  • 4. any/eq マッチャーの混在ルールを守ります。 いずれかの引数でマッチャー(any()、eq()、argThat { … })を使う場合は、すべての引数をマッチャーにする必要があります。値をそのまま any() と混在させると、InvalidUseOfMatchersException: Misplaced or misused argument matcher が発生します。値をそのまま渡す引数は eq(...) で囲みます。
verify(repo).save(any(), eq("identifier"))   // RIGHT — all args are matchers
verify(repo).save(any(), "identifier")        // WRONG — InvalidUseOfMatchersException
  • 5. サスペンド関数は、任意のサスペンドコンテキストからスタブ化します。 Mockito には、サスペンド関数をファーストクラスでサポートする機能がありません。コンパイラーが whenever(mock.suspendFn()) を受け付けるのは、呼び出し箇所自体がサスペンド可能な場合に限られます。runTest { … } はすでにサスペンドコンテキストを提供します。runTest { … } 内でスタブを runBlocking { … } で囲むと、テストディスパッチャーがブロックされ、MainDispatcherRule の使用時にデッドロックが発生する可能性があります。どちらか一方を選び、両方は使わないでください。 これは Mockito の主な使い勝手上の課題です。コードベースでサスペンド関数を多用する場合は、代わりに ../mocking-with-mockk/SKILL.md の採用を検討してください。
@Test fun loadsUser() = runTest {
    whenever(repo.fetchUser(1)) doReturn User("Jane")   // RIGHT — runTest IS a suspend context
    val vm = UserViewModel(repo)
    vm.load(1)
    advanceUntilIdle()
    verify(repo).fetchUser(1)
}

サスペンドしないテスト(runTest を使わないテスト)では、代わりに runBlocking を使います。ただし、両方を同時に使わないでください。

@Test fun loadsUser() = runBlocking {
    whenever(repo.fetchUser(1)) doReturn User("Jane")
    val result = UserService(repo).get(1)
    assertThat(result.name).isEqualTo("Jane")
}
  • 6. Mockito 4.x では、リソースファイルを使ってインラインモックメーカーを有効にします。 Mockito 5.x では、デフォルトですでに有効になっています。これより古いバージョンでは、src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker に次の内容を記述した1行のファイルを配置します。
mock-maker-inline

これは、androidx が compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker、compose/foundation/foundation/.../mockito-extensions/...、および room3/room3-runtime/.../mockito-extensions/... で採用している方法とまったく同じです。もう一つの方法は、Gradle で mockito-core を mockito-inline に置き換えることです(依存関係として mockito-core も取り込まれます)。

インラインモックにも制限があります。final なプライベートメンバー、equals()/hashCode()、String、Class はモック化できません。

  • 7. API < 28 の計装テストでは、final クラスのモック化をガードします。 mockito-android は dexmaker を使用しており、final クラスのモック化をサポートするのは API 28 以降に限られます。これは Mockito のバージョンではなく、dexmaker の制約です。Mockito 5.x にアップグレードしても解消されません。dexmaker が必要とする JVMTI フックが導入されたのは、Android API 28(P)以降です。androidx では、テストクラスに @SdkSuppress(minSdkVersion = 28) を追加し、1行のコメントを添えるのが慣例です。compose/ui/ui/src/androidHostTest/kotlin/androidx/compose/ui/input/IndirectPointerEventWithInputDeviceMockTest.kt にある実例を示します。
import androidx.test.filters.SdkSuppress

@RunWith(MockitoJUnitRunner::class)
@SdkSuppress(minSdkVersion = 28)   // Mocks for final classes can only be done on 28 and higher
class IndirectPointerEventWithInputDeviceMockTest { /* ... */ }
  • 8. 新しいテストクラスには Strictness.STRICT_STUBS を適用します。 Strict stubs は、未使用のスタブや引数の不一致を失敗として検出し、実際にはスタブを使っていないのにテストが「成功」してしまうケースを見つけます。ルールを使って設定します。
@get:Rule val mockitoRule: MockitoRule =
    MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
  • 9. テストスイートが実行され、想定どおりのやり取りが行われることを確認します。 ./gradlew :<module>:testDebugUnitTest --tests com.example.UserViewModelTest。検証に失敗すると、標準的な「Wanted but not invoked」/「Argument(s) are different」という出力が表示されます。通常、これらのメッセージから問題を特定できます。

パターン

パターン:誤った例と正しい例 — Kotlin の when キーワードとの衝突
// WRONG — raw Mockito from Kotlin
import org.mockito.Mockito.`when`

`when`(repo.fetch()).thenReturn(User("Jane"))
// WRONG because: `when` requires backtick-escaping in Kotlin (it is a reserved keyword),
// the import line is awkward, and any() returns Java null which crashes Kotlin
// non-nullable parameters. mockito-kotlin's whenever / null-safe matchers solve both.
// RIGHT — mockito-kotlin DSL
import org.mockito.kotlin.whenever
import org.mockito.kotlin.doReturn

whenever(repo.fetch()) doReturn User("Jane")
パターン:誤った例と正しい例 — 引数キャプターの冗長さ
// WRONG
import org.mockito.ArgumentCaptor

val captor = ArgumentCaptor.forClass(User::class.java)
verify(repo).save(captor.capture())
assertEquals("Jane", captor.value.name)
// WRONG because: ArgumentCaptor.forClass requires .java, captor.value collides with
// Kotlin's value class semantics for some types, and there's no firstValue/lastValue
// accessor for sequences of captures.
// RIGHT
import org.mockito.kotlin.argumentCaptor

val captor = argumentCaptor<User>()
verify(repo, atLeastOnce()).save(captor.capture())
assertEquals("Jane", captor.lastValue.name)
val all: List<User> = captor.allValues
パターン:誤った例と正しい例 — Mockito 4.x での final クラスのモック化
// WRONG — Mockito 4.x without the inline plugin, mocking a Kotlin final class
val service: PaymentService = mock()
// WRONG because: Kotlin classes are final by default. The classic subclass mock-maker
// throws "Cannot mock/spy class com.example.PaymentService — final class". Either
// upgrade to Mockito 5.x (inline by default) or install the plugin file.
// RIGHT — src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
mock-maker-inline
// Mockito 5.x: nothing else to do; mock<PaymentService>() works out of the box.
パターン:誤った例と正しい例 — Strict stubs で未使用のスタブを検出
// WRONG — silent passing test
class MyTest {
    @get:Rule val mockitoRule = MockitoJUnit.rule()   // default lenient

    @Test fun loadsUser() {
        whenever(repo.findById(7L)) doReturn User("Jane")
        val vm = UserViewModel(repo)
        vm.load(8L)                                    // wrong id — stub is never used
        verify(repo).findById(any())                   // passes because findById was called
    }
}
// WRONG because: the test claims to verify behaviour for id=7 but actually exercises id=8.
// The stub is dead. STRICT_STUBS catches this with PotentialStubbingProblem.
// RIGHT
@get:Rule val mockitoRule: MockitoRule =
    MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)

// Now PotentialStubbingProblem fires at runtime if the stub argument doesn't match
// what the SUT actually calls.

必須ルール

  • 必須:Kotlin のテストには org.mockito:mockito-core:5.x と org.mockito.kotlin:mockito-kotlin:5.x の両方を追加してください。DSL アーティファクトは、最新の Kotlin コードベースでは必須です。
  • 必須:org.mockito.Mockito の `when`(...) ではなく、org.mockito.kotlin.* の whenever(...) を使用してください。キーワードをエスケープする形式はコードの臭いです。
  • 必須:Kotlin のテストには org.mockito.kotlin の any()/anyOrNull()/eq() を使用してください。Mockito の any() は Java の null を返すため、実行時に Kotlin の非 null パラメーターでクラッシュします。
  • 必須:サスペンド関数はサスペンドコンテキストからスタブ化してください。runTest { whenever(...) doReturn ... }(仮想時間と統合されるため推奨)を使うか、runTest を使わないセットアップでは runBlocking { ... } を使います。runTest の内部で runBlocking をネストしてはいけません。テストディスパッチャーがブロックされ、MainDispatcherRule のもとでデッドロックする可能性があります。Mockito にはサスペンドスタブ化のためのネイティブな仕組みがないため、どちらのサスペンドコンテキストでも問題ありません。
  • 必須:MockitoAnnotations.openMocks(this) を使用し、返された AutoCloseable を @After でクローズしてください。非推奨の MockitoAnnotations.initMocks(this) は使用してはいけません。
  • 必須:Kotlin の final クラスをモックする Mockito 5.x より前のプロジェクトでは、内容が文字どおり mock-maker-inline の mockito-extensions/org.mockito.plugins.MockMaker を用意してください。Mockito 5.x では inline がデフォルトで有効なため、何も必要ありません。
  • 必須:mockito-android に対する androidTestImplementation として実行する場合、final クラスをモックするテストクラスには @SdkSuppress(minSdkVersion = 28) を付けてください。
  • 必須:1 つの verify/whenever 呼び出し内で、マッチャーの使い方を統一してください。すべてマッチャーを使うか、マッチャーを使わないかのどちらかにします。混在させる場合は、値を eq(...) でラップしてください。
  • 必須:振る舞いを持つ協力オブジェクトには、モックよりフェイクを優先してください(developer.android.com/training/testing/fundamentals/test-doubles:「フェイクが推奨されます」)。Mockito は相互作用の検証に使い、振る舞いの再実装には使わないでください。
  • 必須:同じファイル内で Mockito.mock(...) と org.mockito.kotlin.mock<T>() を混在させてはいけません。どちらか一方を選び、一貫して使用してください。Kotlin DSL を推奨します。
  • 推奨:デッドスタブや引数の不一致を検出するため、Strictness.STRICT_STUBS を適用してください。lenient モードはバグを隠します。

検証

  • ./gradlew :<module>:testDebugUnitTest --tests <YourTestClass> が成功する。
  • grep -rn "org.mockito.Mockito.\when`" src/test src/androidTestの結果が 0 件である(代わりにwhenever` を使用する)。
  • grep -rn "MockitoAnnotations.initMocks" src/test src/androidTest の結果が 0 件である(代わりに openMocks を使用する)。
  • grep -rn "ArgumentCaptor.forClass" src/test src/androidTest が新しいコードで 0 件である(代わりに argumentCaptor<T>() を使用する)。
  • プロジェクトが Mockito 4.x を使用しており、Kotlin の final クラスをモックする場合、ファイル src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker が存在し、その内容が文字どおり mock-maker-inline である。
  • MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS) が適用されている(または JUnit5 のアノテーション @MockitoSettings(strictness = Strictness.STRICT_STUBS) が適用されている)。
  • Strictness.STRICT_STUBS のもとで、未使用のスタブが報告されない。
  • final クラスを扱うインストルメンテーションテストに @SdkSuppress(minSdkVersion = 28) が付いている。

参考資料

コメント