Mocking with Mockito — The androidx-Native Mocking Stack
Mockito is the mocking framework androidx itself uses — 400+ test files import org.mockito across the AOSP checkout, while io.mockk returns zero hits. New Android code that wants to "match what Google does" picks Mockito + the mockito-kotlin Kotlin DSL. This skill documents the exact dependency matrix, the three setup styles, the Kotlin DSL surface, and the inline-mock-maker plumbing required for Kotlin final classes.
When to use this skill
- The user is starting a new test suite in a Kotlin Android module and asks "what mocking library should I use".
- The user reports
Cannot mock/spy class … final classfrom Mockito 4.x and needs themock-maker-inlineconfiguration. - The user reports
InvalidUseOfMatchersExceptionfrom mixing raw values withany()/eq(). - The user is wrestling with the
whenkeyword clash in Kotlin (`when`(mock.foo())) and wants thewheneverDSL. - The user wants
argumentCaptor<T>()withfirstValue/lastValue/allValuesinstead ofArgumentCaptor.forClass(...). - The user is migrating from
MockitoAnnotations.initMocks(this)toopenMocks(this)or theMockitoJUnit.rule()approach. - The user is mocking a
finalclass on an instrumented test on API < 28.
When NOT to use this skill
- The user wants
coEvery/coVerifyfor suspend functions, deep singleton mocking viamockkObject, or static mocking viamockkStatic— use../mocking-with-mockk/SKILL.md. - The user is choosing between fakes and mocks at the design level — use
../../../fundamentals/doubles/picking-test-doubles/SKILL.md. Per /test-doubles, fakes are preferred over mocks for behaviour-heavy collaborators. - The runner / Gradle matrix isn't set up yet — start with
../../runner/configuring-junit4-on-android/SKILL.md. - The user wants
runTest-aware testing for coroutines — see../../coroutines/testing-coroutines-with-runtest/SKILL.md.
Prerequisites
- The base test wiring from
../../runner/configuring-junit4-on-android/SKILL.mdis already in place (@RunWith(AndroidJUnit4::class),androidx.test.ext:junit:1.3.0, JUnit 4.13.2). - A Kotlin module on JDK 11+ (Mockito 5.x raised the JDK floor to Java 11).
- A
MainDispatcherRuleis installed if any code-under-test touchesDispatchers.Main— see the runner skill.
Workflow
- 1. Add the Mockito + mockito-kotlin coordinates. Mockito 5.x is the recommended baseline because the inline mock-maker is enabled by default — no plugin file or
mockito-inlineswap needed for Kotlin final classes:
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 is NOT a fork — it is a thin DSL helper layer that delegates straight to mockito-core. Both must be on the classpath.
- 2. Pick a mock-lifecycle style. There are three idiomatic options. Prefer the rule for new code; reserve
openMocksfor cases where the test class can't claim@RunWith.
// (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)
The deprecated MockitoAnnotations.initMocks(this) returns void and leaks inline-mock resources. Always use openMocks(this) and close the returned AutoCloseable in @After.
- 3. Declare mocks and stub them with the mockito-kotlin DSL. The Kotlin-friendly entry points live under
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))
}
}
Highlights:
mock<T>()is reified — noT::class.javaceremony.mock { on { … } doReturn … }is a builder block — collapses a chain ofwhenevercalls when stubbing at construction time.whenever(...)replaces raw Mockito's`when`(...)(which requires backticks becausewhenis a Kotlin keyword).argumentCaptor<T>()returns a captor withfirstValue/secondValue/lastValue/allValuesproperties — nogetValue()ceremony.any(),anyOrNull(),eq()fromorg.mockito.kotlin.*are null-safe in Kotlin (raw Mockito'sany()returns Javanull, which crashes a Kotlin non-nullable parameter).4. Honour the any/eq matcher mixing rule. If ANY argument uses a matcher (
any(),eq(),argThat { … }), ALL arguments must be matchers. Mixing raw values withany()raisesInvalidUseOfMatchersException: Misplaced or misused argument matcher. Wrap raw values ineq(...):
verify(repo).save(any(), eq("identifier")) // RIGHT — all args are matchers
verify(repo).save(any(), "identifier") // WRONG — InvalidUseOfMatchersException
- 5. Stub suspend functions from any suspend context. Mockito does not have first-class suspend support. The compiler accepts
whenever(mock.suspendFn())only when the call site is itself suspending.runTest { … }already provides that suspend context — wrapping a stub inrunBlocking { … }insiderunTest { … }blocks the test dispatcher and can deadlock underMainDispatcherRule. Pick one or the other, not both. This is the chief ergonomic friction with Mockito; if the codebase is suspend-heavy, weigh../mocking-with-mockk/SKILL.mdinstead.
@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)
}
For non-suspending tests (no runTest), use runBlocking instead — but never both:
@Test fun loadsUser() = runBlocking {
whenever(repo.fetchUser(1)) doReturn User("Jane")
val result = UserService(repo).get(1)
assertThat(result.name).isEqualTo("Jane")
}
- 6. For Mockito 4.x, install the inline mock-maker via the resource file. Mockito 5.x already enables it by default. For older versions, drop a single-line file at
src/test/resources/mockito-extensions/org.mockito.plugins.MockMakercontaining:
mock-maker-inline
This is exactly what androidx ships in compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker, compose/foundation/foundation/.../mockito-extensions/..., and room3/room3-runtime/.../mockito-extensions/.... The alternative is swapping mockito-core for mockito-inline in Gradle (which transitively pulls mockito-core).
Inline still has limits: final private members, equals()/hashCode(), String, and Class cannot be mocked.
- 7. For instrumented tests on API < 28, guard final-class mocks.
mockito-androiduses dexmaker, which only supports final-class mocking on API 28+. This is a dexmaker constraint, not a Mockito version constraint — upgrading to Mockito 5.x does not lift it; only Android API 28 (P) introduced the JVMTI hooks dexmaker needs. The androidx convention is to add@SdkSuppress(minSdkVersion = 28)to the test class with a one-line comment. Real example fromcompose/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. Apply
Strictness.STRICT_STUBSfor new test classes. Strict stubs surface unused stubs and argument mismatches as failures, catching tests that "pass" because they don't actually exercise the stub. Wire it via the rule:
@get:Rule val mockitoRule: MockitoRule =
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
- 9. Verify the suite runs and produces the expected interactions.
./gradlew :<module>:testDebugUnitTest --tests com.example.UserViewModelTest. Failed verifications produce the canonical "Wanted but not invoked" / "Argument(s) are different" output — these messages are usually self-diagnosing.
Patterns
Pattern: WRONG vs RIGHT — Kotlin when keyword clash
// 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")
Pattern: WRONG vs RIGHT — argument captor verbosity
// 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
Pattern: WRONG vs RIGHT — final-class mocking on Mockito 4.x
// 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.
Pattern: WRONG vs RIGHT — strict stubs catch the dead stub
// 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.
Mandatory rules
- MUST add both
org.mockito:mockito-core:5.xANDorg.mockito.kotlin:mockito-kotlin:5.xfor Kotlin tests. The DSL artifact is non-optional in any modern Kotlin codebase. - MUST use
whenever(...)fromorg.mockito.kotlin.*, not`when`(...)fromorg.mockito.Mockito. The keyword-escape form is a code smell. - MUST use
org.mockito.kotlin'sany()/anyOrNull()/eq()for Kotlin tests. Raw Mockito'sany()returns Javanulland crashes Kotlin non-nullable parameters at runtime. - MUST stub suspend functions from a suspending context — either
runTest { whenever(...) doReturn ... }(preferred, integrates with virtual time) orrunBlocking { ... }for non-runTestsetups. MUST NOT nestrunBlockinginsiderunTest— it blocks the test dispatcher and can deadlock underMainDispatcherRule. Mockito has no native suspend-stubbing surface; either suspend context is sufficient. - MUST use
MockitoAnnotations.openMocks(this)and close the returnedAutoCloseablein@After. MUST NOT use the deprecatedMockitoAnnotations.initMocks(this). - MUST ship
mockito-extensions/org.mockito.plugins.MockMakerwith literal contentsmock-maker-inlinefor any pre-5.x project that mocks Kotlin final classes. Mockito 5.x ships inline by default and needs nothing. - MUST annotate test classes that mock final classes with
@SdkSuppress(minSdkVersion = 28)when they run asandroidTestImplementationagainstmockito-android. - MUST keep matchers consistent within a single
verify/whenevercall: all matchers OR no matchers. Wrap raw values ineq(...)to mix. - MUST prefer fakes over mocks for collaborators with behaviour (per developer.android.com/training/testing/fundamentals/test-doubles: "fakes ... are preferred"). Use Mockito for verifying interactions, not for re-implementing behaviour.
- MUST NOT mix
Mockito.mock(...)andorg.mockito.kotlin.mock<T>()in the same file — pick one and be consistent. Prefer the kotlin DSL. - PREFERRED: apply
Strictness.STRICT_STUBSto catch dead stubs and argument mismatches. Lenient mode hides bugs.
Verification
-
./gradlew :<module>:testDebugUnitTest --tests <YourTestClass>passes. -
grep -rn "org.mockito.Mockito.\when`" src/test src/androidTestreturns NO matches (usewhenever` instead). -
grep -rn "MockitoAnnotations.initMocks" src/test src/androidTestreturns NO matches (useopenMocksinstead). -
grep -rn "ArgumentCaptor.forClass" src/test src/androidTestreturns NO matches in new code (useargumentCaptor<T>()instead). - If the project is on Mockito 4.x and mocks a Kotlin final class, the file
src/test/resources/mockito-extensions/org.mockito.plugins.MockMakerexists with literal contentsmock-maker-inline. -
MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)is applied (or the@MockitoSettings(strictness = Strictness.STRICT_STUBS)JUnit5 annotation). - No unused stubs reported under
Strictness.STRICT_STUBS. - Final-class instrumented tests carry
@SdkSuppress(minSdkVersion = 28).
References
- Mockito reference: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
- mockito-kotlin: https://github.com/mockito/mockito-kotlin
- Mockito 5 release notes: https://github.com/mockito/mockito/releases/tag/v5.0.0
- Android Developers — Local tests: https://developer.android.com/training/testing/local-tests
- Android Developers — Test doubles: https://developer.android.com/training/testing/fundamentals/test-doubles
androidx/car/app/app/src/test/java/androidx/car/app/serialization/ListDelegateTest.kt— canonical 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)+ raw`when`(legacy form).androidx/compose/material/material/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— literalmock-maker-inlinecontents.androidx/compose/foundation/foundation/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— same plugin file convention.androidx/room3/room3-runtime/src/androidHostTest/resources/mockito-extensions/org.mockito.plugins.MockMaker— same plugin file convention.