skip to content

A Kotlin team enables parallel JUnit 5 execution on a suite that uses MockK's MockKExtension, and mocks start losing their stubs mid-test. What does the extension do that breaks under parallel execution, and how would you decide what to do about it?

level: principalimportance: nice to knowfreq 16%

answer

  1. unmockkAll/clearAllMocks are JVM-wide, not test-scoped
  2. symptom: stubs vanish mid-test, non-reproducible
  3. @MockKExtension.RequireParallelTesting / keepmocks suppress cleanup
  4. strictness checks are NOT suppressed by those switches
  5. global patching stays shared — keep those classes serial

basics

~20 s

Its end-of-test cleanup calls process-global unmockkAll() and clearAllMocks(), which wipe every mock in the JVM — including ones a concurrently running test is still using. MockK's answer is @MockKExtension.RequireParallelTesting (or KeepMocks) to suppress that cleanup, after which isolation is yours.

solid answer

~50 s

MockKExtension's cleanup is **global, not test-scoped**: `unmockkAll()` and `clearAllMocks()` touch every registered mock and all `mockkObject`/`mockkStatic`/`mockkConstructor` patching in the JVM. Serially that is exactly right; run tests concurrently and one test's teardown clears another test's live mocks — the symptom is stubs vanishing mid-test, seemingly at random. MockK's switch is `@MockKExtension.RequireParallelTesting` on the class, or the configuration parameter `mockk.junit.extension.requireParallelTesting=true`; `@MockKExtension.KeepMocks` suppresses the same calls. Note the strictness checks (`ConfirmVerification`, `CheckUnnecessaryStub`) are *not* suppressed with them. The judgment part: once cleanup is off you own isolation. Global patching is inherently shared state, so classes using `mockkObject`/`mockkStatic`/`mockkConstructor` must stay serial no matter what; plain per-test instance mocks are naturally isolated because JUnit builds a new test instance per method. I would keep the suite serial by default, parallelise only the plain-mock classes, and measure whether the wall-clock win justifies the class of failure I am importing.

code

kotlin · 13 lines
kotlin
@ExtendWith(MockKExtension::class)
@MockKExtension.RequireParallelTesting
class PricingRulesTest {

    @MockK
    lateinit var rates: RateTable // per-test instance mock: safe to run concurrently

    @Test
    fun `applies discount`() {
        every { rates.of("EUR") } returns 0.9
        assertEquals(90, Pricing(rates).convert(100, "EUR"))
    }
}

go deeper

for a junior

It is enough to know MockK's cleanup affects all mocks in the JVM, so running tests in parallel can make them interfere.

for a middle

Name the two calls in the extension's cleanup, explain why they are global, and point at @MockKExtension.RequireParallelTesting as the opt-out.

for a senior

Diagnose from the symptom, know that suppressing cleanup shifts isolation onto you, and separate instance mocks (safe) from global patching (never safe concurrently).

for a principal

Answer as a suite strategy: measure the payoff, segment parallel vs serial classes, encode the rule in a project annotation, keep strictness checks off the parallel set, and prefer removing global patching over configuring around it.

## The mechanism that breaks MockKExtension ends a test (or a class, under the per-class instance lifecycle) by calling `unmockkAll()` followed by `clearAllMocks()`. Neither is scoped to the test that triggered it: - `unmockkAll()` reverses every global patch in the process — objects patched with `mockkObject`, file facades and Java statics patched with `mockkStatic`, constructors patched with `mockkConstructor`. - `clearAllMocks()` resets answers, recorded calls and child mocks on **every** registered mock in the JVM. Under serial execution that is the desired blast radius: whatever the finished test dirtied is cleaned. Under parallel execution the same call is a cross-test data race. Test A finishes and clears the world while test B, on another thread, is halfway between `every { }` and the call it stubbed. B then fails with "no answer found" for something it demonstrably stubbed, or silently loses its recorded calls so a `verify` reports zero invocations. The failures are non-deterministic, thread-timing dependent, and almost never reproduce when you rerun the single test — which is exactly why people misdiagnose them as MockK bugs. ## What MockK offers `@MockKExtension.RequireParallelTesting` (class level) or the JUnit Platform configuration parameter `mockk.junit.extension.requireParallelTesting=true` tells the extension to skip those global calls. `@MockKExtension.KeepMocks` — allowed on a class *or* a single method, or `mockk.junit.extension.keepmocks=true` — suppresses the same two calls, though its intent is "I want the mock state to survive" rather than "I run in parallel". One precise detail worth carrying: suppressing cleanup does **not** suppress the strictness checks. If a class also carries `@MockKExtension.ConfirmVerification` or `@MockKExtension.CheckUnnecessaryStub`, those still run — and they too look at *all* registered mocks, so under parallelism they can observe another test's interactions. Strict checking and parallel execution are a bad pairing. ## What you own once cleanup is off **Instance mocks are mostly fine on their own.** With JUnit's default per-method test instance, each test gets its own annotated mocks, so there is no sharing to protect. Turning cleanup off costs you only the reset of mocks nobody else can see — plus memory held until the JVM ends. **Global patching is not fine, ever.** `mockkObject`, `mockkStatic` and `mockkConstructor` mutate state shared by the whole process. Two concurrent tests patching the same object, or one patching while another calls the real thing, cannot be made safe by any extension option — the sharing is in the production construct, not in MockK's bookkeeping. And with cleanup suppressed, a patch installed by one test now leaks to every later test in the fork. **You need a replacement teardown.** If a class both patches globals and opts out of cleanup, it must unpatch itself explicitly, and it must not run concurrently with anything that touches the same construct. ## The judgment call I would frame it as: *what am I buying, and what class of failure am I importing?* 1. **Measure first.** If mock-heavy unit tests are seconds of the build and integration tests are minutes, parallelising them buys nothing. Fork-level parallelism at the build-tool layer often gives most of the win with none of this problem, because each fork is a separate JVM with its own global state. 2. **Segment the suite.** Classes that use only plain instance mocks are candidates for concurrency. Classes that patch objects, statics or constructors get pinned to serial execution using the test framework's execution-mode controls, and stay there permanently. 3. **Make the constraint enforceable.** "Don't use mockkStatic in a parallel class" as a code-review convention decays. A single project annotation that combines extension registration, serial execution and normal cleanup — applied to every global-patching class — encodes it once. 4. **Do not mix strictness with parallelism.** Global `confirmVerified()`/`checkUnnecessaryStub()` observe all mocks; concurrent tests make them flaky. Pick one. 5. **Prefer removing the need.** Most `mockkStatic`/`mockkObject` usage exists because production code reaches for a global. Injecting that dependency instead deletes the whole category of problem, gives faster tests, and is usually the better place to spend the effort. ## The short version to say out loud The extension's cleanup is JVM-wide, so parallel tests clear each other. `@MockKExtension.RequireParallelTesting` suppresses it, but that only removes MockK's own interference — anything patched globally remains genuinely shared. So: parallelise the classes that only use instance mocks, keep global-patching classes serial, keep strictness checks out of the parallel set, and be sure the wall-clock saving is worth the flakiness budget.

  • With @MockKExtension.RequireParallelTesting set, is it now safe to use mockkStatic in those classes?
    No. The annotation only stops MockK's extension from clearing global state at the wrong moment; it does not make the patching itself thread-safe. mockkStatic, mockkObject and mockkConstructor mutate process-wide constructs that concurrent tests share by definition. Those classes must stay serial, and because cleanup is suppressed they also need explicit unmocking of their own.
  • How would you get build-time wins without this risk?
    Parallelise at the JVM-fork level in the build tool, so each fork has its own global state and MockK's cleanup stays correct within it. Beyond that, attack the causes: slow integration tests usually dominate, and removing mockkStatic/mockkObject usage by injecting the dependency makes tests both faster and safely parallelisable.

saying these in an interview costs you the question

  • Believing MockKExtension's cleanup is scoped to the test that triggered it.
  • Assuming @MockKExtension.RequireParallelTesting makes mockkStatic/mockkObject safe to use concurrently.
  • Thinking suppressing cleanup also disables ConfirmVerification/CheckUnnecessaryStub.
  • Diagnosing 'stub disappeared mid-test' as a MockK defect instead of cross-test global teardown.
  • Turning on parallel execution suite-wide without segmenting classes that patch global state.

context