skip to content

A MockK-based test passes on its own but fails when the whole class or suite runs, complaining about a stubbed value or a call count that belongs to a different test. What MockK state makes that possible, and how would you track it down?

level: seniorimportance: should knowfreq 35%

answer

  1. passes alone, fails in suite = leaked state
  2. mockkObject/Static/Constructor = JVM-wide install
  3. clearAllMocks does not un-patch
  4. bisect: suspect + failing test in sequence
  5. fix: scoped blocks or symmetric unmockk

basics

~20 s

Order dependence points at state that outlives a test: JVM-wide registrations from mockkObject/mockkStatic/mockkConstructor, and mocks held in shared or static fields. Bisect by running the suspects in sequence, then add unmockkAll teardown or scoped patches.

solid answer

~50 s

Order-dependent failures mean state survived a test boundary. In MockK there are two sources. **Global registrations.** `mockkObject`, `mockkStatic` and `mockkConstructor` install JVM-wide patches keyed by the target. They are not scoped to a method or a class — they persist until an `unmockk*` call, and test runs normally share one JVM across classes. Any later test touching that object or those statics runs against instrumented code, sometimes with stubs still attached. **Shared double instances.** Mocks stored in a companion object, a static holder, or a fixture that outlives one test keep their stub table and their recorded-call log, so counts accumulate. Diagnosis: reproduce deliberately — run the failing test alone (green), then the suspect plus the failing test (red). That pins the culprit. Then grep for `mockkObject|mockkStatic|mockkConstructor` without a matching `unmockk`, and for mocks held outside per-test scope. Fix: `unmockkAll()` in teardown or scoped `mockkObject(X) { ... }` blocks, and `clearMocks` for genuinely shared doubles.

code

kotlin · 14 lines
kotlin
// Leaks: the registration outlives the test method and the class
@Test
fun leaky() {
    mockkObject(FeatureFlags)
    every { FeatureFlags.enabled("beta") } returns true
    assertTrue(service.betaPath())
}

// Cannot leak: the patch is removed when the block exits, even on failure
@Test
fun scoped() = mockkObject(FeatureFlags) {
    every { FeatureFlags.enabled("beta") } returns true
    assertTrue(service.betaPath())
}

go deeper

for a junior

Recognise the pattern: passing alone but failing in a suite means leftover state, not flakiness.

for a middle

Name the two sources — JVM-wide mockkObject/mockkStatic/mockkConstructor registrations and shared mock instances — and the matching teardown.

for a senior

Show the diagnostic loop (reproduce the pairing, read the message, grep for unmatched patches, verify by re-running the pairing) and prefer scoped patches over remembered teardown.

for a principal

Treat it as a suite-design defect: make leaking structurally impossible with scoped blocks and one enforced teardown path, and reduce reliance on global patching altogether.

## The signature of the bug Three symptoms, all meaning the same thing: - Passes alone, fails in the suite (or the reverse). - Passes in your IDE, fails in the pipeline, where a different set or order of tests runs. - A `verify(exactly = 1)` reports two calls, one of which you never made in this test. All three say: state crossed a test boundary. ## Source one: JVM-wide registrations `mockkObject(SomeObject)`, `mockkStatic("com.acme.UtilsKt")` and `mockkConstructor(SomeClass::class)` do not create a local double you hold and drop. They register a patch in a process-wide table keyed by the target. Nothing in the test framework's lifecycle removes it — not the end of the method, not the end of the class. A normal run executes many test classes in one JVM, so the patch is visible to all of them. Two distinct consequences: 1. **Stubs may persist.** If nothing cleared them, the object still answers with the previous test's configured value. 2. **Instrumentation persists even after clearing.** `clearAllMocks()` resets stubs but leaves the registration, so the object is still routed through MockK and still recording. A later test that patches the same target inherits a registration it did not install, and whoever tears down first affects the other. ## Source two: doubles that outlive a test A mock stored in a `companion object`, in a static holder, or in any fixture shared across tests keeps its state: its stub table and its recorded-call log. Counts accumulate across tests, and yesterday's stub answers today's call. The same applies to a spy created once and reused. ## Diagnosing methodically **1. Reproduce on purpose.** Run the failing test alone — it should pass. Then run it immediately after each suspect. When one pairing turns it red, you have the culprit, and the diff between the two runs is small enough to read. **2. Read the failure literally.** "No answer found" for a call you did stub often means something cleared it. A stale *value* means someone else's stub is answering. A count that is too high means the record was never reset or the double is shared. **3. Grep the suspects.** Search the module for `mockkObject`, `mockkStatic`, `mockkConstructor` and check each has a matching `unmockk*` on all paths, including when the test throws. Then search for mock fields declared outside per-test scope. **4. Name your doubles.** Creating mocks with a `name` argument makes MockK's messages identify which instance is involved, which is often the fastest way to discover that two tests are talking to the same object. **5. Confirm the fix by re-running the pairing**, not just the failing test alone — running it alone was already green and proves nothing. ## Fixing it, in order of preference 1. **Scope the patch.** MockK 1.13's block forms — `mockkObject(X) { ... }`, `mockkStatic("...") { ... }`, `mockkConstructor(X::class) { ... }` — remove the registration when the block exits, including on exceptions. The leak becomes structurally impossible. 2. **Symmetric teardown.** If you must patch for the whole test, undo it in teardown with the targeted `unmockkObject`/`unmockkStatic`/`unmockkConstructor`, or `unmockkAll()` where a blanket sweep is acceptable. Put it in one shared place so nobody has to remember. 3. **Stop sharing doubles.** Create mocks per test; it costs almost nothing and removes the whole category. Where a fixture genuinely must be shared, `clearMocks` it deliberately and document why. 4. **Do not patch globals at all** where a collaborator can be injected instead — the leak-prone mechanisms are the ones that reach into shared, process-wide state. ## The mental model to carry Regular mocks are values: they die with the test. Object, static and constructor mocks are *installations*: they live in the process until removed. Isolation problems in MockK almost always trace back to treating an installation like a value.

  • Would adding clearAllMocks() to teardown fix a leak caused by mockkObject?
    Only partially, and misleadingly. It removes the stale stubs, so the most visible symptom disappears, but the object remains patched for the rest of the JVM's life — MockK is still in its call path and still recording. The correct teardown is unmockkObject or unmockkAll, or better, a scoped mockkObject block.
  • How would you stop this class of bug from recurring across a large suite?
    Make the safe path the default: prefer scoped mockkObject/mockkStatic blocks so a registration cannot outlive its test, put any remaining teardown in one shared base fixture rather than in individual tests, and create mocks per test instead of sharing them. Reviewing new occurrences of the global patching functions keeps the count from growing.

saying these in an interview costs you the question

  • Concluding the test is 'flaky' rather than order-dependent
  • Assuming MockK resets everything between test methods automatically
  • Using clearAllMocks as the fix for a leaked object/static registration
  • Only re-running the failing test alone to 'confirm' a fix
  • Storing mocks in companion objects and expecting fresh state each test

context