mocking-with-mockito について
Kotlin Android テストで Mockito と mockito-kotlin を使うためのスキルガイドです。テストを書く Android 開発者向けに、依存関係、モックのライフサイクル、Kotlin DSL を説明し、final クラスのモックや引数マッチャー、コルーチンテストの注意点も扱います。
主な機能
- 01Mockito と mockito-kotlin の設定
- 02モックのライフサイクル方式の選択
- 03Kotlin DSL によるモック作成と引数取得
- 04引数マッチャーの適切な組み合わせ
- 05final クラスのモック設定
- 06厳格なスタブ検証の有効化
活用シーン
- 01Kotlin Android のユニットテストでモックを作成
- 02final クラスをモックできない問題の解決
- 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()))に悩んでいて、wheneverDSL を使いたい場合。 - ユーザーが
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)が付いている。
参考資料
- Mockito リファレンス:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
- mockito-kotlin:https://github.com/mockito/mockito-kotlin
- Mockito 5 リリースノート:https://github.com/mockito/mockito/releases/tag/v5.0.0
- Android Developers — ローカルテスト:https://developer.android.com/training/testing/local-tests
- Android Developers — テストダブル:https://developer.android.com/training/testing/fundamentals/test-doubles
androidx/car/app/app/src/test/java/androidx/car/app/serialization/ListDelegateTest.kt— mockito-kotlin DSL の標準例(mock<T>()、argumentCaptor<T>()、verify(... atLeastOnce())、lastValue)。androidx/compose/ui/ui/src/androidHostTest/kotlin/androidx/compose/ui/input/IndirectPointerEventWithInputDeviceMockTest.kt—@RunWith(MockitoJUnitRunner::class)+@SdkSuppress(minSdkVersion = 28)+ 生の`when`(旧形式)。androidx/compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker—mock-maker-inlineのみを記述したファイル。androidx/compose/foundation/foundation/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— 同じプラグインファイルの形式。androidx/room3/room3-runtime/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— 同じプラグインファイルの形式。