MockK's mockkObject patches a singleton in place rather than substituting it. Concretely, who can observe that patch while it is active, and what does unmockkObject give back — and what does it not give back?
answer
- patch = property of the loaded class, not of a reference
- visible to all threads, all parallel tests, all holders
- lives until explicit unmockk — never implicitly
- restores behaviour, not state or side effects
- block-scoped mockkObject(Obj) { } narrows the window
basics
~20 sEverything in that JVM sees it: all threads, all concurrently running tests, all reference holders, and any production path reaching the singleton. There is no per-test or per-thread scoping. unmockkObject restores real behaviour only — not the object's mutated state, nor side effects real code already performed.
solid answer
~50 sBecause MockK instruments the existing instance instead of substituting one, the patch is a property of the loaded class in that JVM. Its visibility is therefore process-wide: every thread, every test running concurrently in the same JVM, every field or lambda that captured the reference earlier, and any production code path that reaches the singleton. There is no notion of "my test's version of this object". The patch lives from the `mockkObject` call until an explicit `unmockkObject`/`unmockkAll`, or the exit of the block-scoped `mockkObject(Obj) { ... }` form — not until the end of the test method. Nothing ends it implicitly. Unmocking restores behaviour: stubs are gone, real methods run again. It does not roll back the world. Property mutations made while the object was patched persist, side effects that unstubbed real code performed persist, and a mid-test unmock silently reverts collaborators still relying on the stub.
code
kotlin · 12 linesobject SessionCache {
var hits = 0
fun get(id: String): Session? { hits++; return load(id) }
}
mockkObject(SessionCache) {
SessionCache.get("a") // unstubbed -> real code runs, hits++
every { SessionCache.get("b") } returns null
assertNull(SessionCache.get("b"))
} // unmocked here
assertEquals(1, SessionCache.hits) // mutation survives the restorego deeper
Know that the patch is global to the JVM and lasts until you undo it explicitly, not until the test ends.
Enumerate who can see it — threads, parallel tests, existing reference holders — and use the block-scoped form to bound the window.
Diagnose order- and parallelism-dependent failures as patch leakage, and distinguish restoring behaviour from restoring state or side effects.
Treat object patching as shared mutable state in the test process, and set expectations about which singletons may be patched, by whom, and under what execution model.
## Why the scope is process-wide Substitution-based mocking is naturally scoped: you build a fake, hand it to one object, and only that object is affected. `mockkObject` cannot work that way, because a Kotlin `object` has no injection point — call sites reference the singleton directly. MockK's answer is to instrument the class so calls on that one existing instance dispatch through its handler. The consequence is that the "seam" is not a reference anyone holds; it is the loaded class itself. That is what makes the blast radius global. ## Who observes it While the patch is active, it is visible to: * **Every thread in the process.** A worker started earlier, a scheduled task, a coroutine on any dispatcher — all of them hit the patched behaviour. * **Every test running concurrently in the same JVM.** If the suite executes tests in parallel within one JVM, another test touching the same singleton sees your stubs, and you may see theirs. * **Every existing reference holder.** Identity did not change, so a field or lambda that captured the object minutes ago is affected without any re-wiring. This is the property that makes the technique work at all for legacy code. * **Production code paths you did not think about.** Anything reachable from the code under test that touches the singleton is patched, not just the calls your test makes. ## When it starts and ends It starts at `mockkObject` and ends only at an explicit undo. Nothing about a test method ending, an assertion failing, or an exception propagating removes it. Three ways to end it, in increasing order of reliability: ``` // 1. explicit, but skipped if the test throws before reaching it mockkObject(Clock); ...; unmockkObject(Clock) // 2. teardown / finally — survives failures try { mockkObject(Clock); ... } finally { unmockkObject(Clock) } // 3. block-scoped overload — unmocks on exit mockkObject(Clock) { ... } ``` The narrow window matters for a reason beyond hygiene: while the patch is active, *other* code is affected, so the shortest possible window is also the smallest possible interference. ## What restoring actually restores `unmockkObject(Obj)` removes the patch: stubs are discarded and real implementations run again. What it explicitly does not do: * **It does not reset the object's state.** If a real, unstubbed method ran during the patched window and incremented a counter, opened a cache entry, or flipped an `initialized` flag, that mutation stands. Object mocks are spies, so unstubbed real code runs by default — this is a common source of leakage even in tests that unmock correctly. * **It does not undo side effects.** Files written, rows inserted, messages published by real code during the window remain. * **It does not repair collaborators mid-flight.** Unmocking while another thread or a still-running coroutine is relying on the stubbed behaviour flips that code back to reality at an arbitrary instant. The resulting failure appears in the other test, not yours. * **It says nothing about other mocks.** Stubs recorded on ordinary mocks or on other patched objects are untouched; each target is undone independently (`unmockkAll` is the blunt version, and under parallel execution it is dangerous precisely because it undoes work other running tests still need). ## Diagnosing leakage The symptoms are distinctive. A test passes alone and fails in the suite, or vice versa. Failures move when test order changes or when the runner's parallelism setting changes. A test that never mentions a singleton fails because it received a fixture value belonging to a different scenario. A stub "stops working" halfway through because another test unmocked the same object. All four are the same underlying story: shared, process-global state whose lifetime is not bounded by the test. ## Practical containment Keep the patched window as small as the assertion needs, preferably with the block-scoped form. Do not patch objects that carry mutable state you also depend on, because restoring behaviour will not restore that state. Do not patch a shared singleton in tests that run in parallel with others touching it. And prefer patching an object you own over one owned by a framework or another team — the further the singleton is from the code under test, the wider the set of paths you have quietly redefined.
- A test that never mentions a mocked object fails only when the whole suite runs. How does that happen?Another test patched a singleton both tests reach and did not undo it, or undid it while your test was mid-flight. Because the patch belongs to the loaded class, its stubs are visible everywhere in the JVM until explicitly removed. The tell is order- and parallelism-dependence: the failure moves when the runner's order or thread count changes.
- Is unmockkAll() in a shared teardown a safe default?It is safe in a sequential suite and actively harmful in a parallel one. It removes every patch in the process, including ones other tests currently executing still depend on, producing failures in tests that look innocent. Prefer undoing exactly what you patched, ideally with the block-scoped form so the window closes where it opened.
You are not swapping a book on your own desk; you are editing the single library copy everyone in the building shares. Putting the original text back does not un-read the edited pages anyone already used.
saying these in an interview costs you the question
- Believing the patch is scoped to the test method or the test thread
- Expecting the patch to end automatically when the test returns or throws
- Thinking unmockkObject also resets properties the object mutated
- Using unmockkAll in a shared teardown while tests run in parallel
- Assuming references captured before the patch are somehow unaffected