skip to content

In a JUnit 5 test class annotated with @ExtendWith(MockKExtension::class), what work does MockK's extension actually perform, and at which points of the test lifecycle does it perform it?

level: middleimportance: must knowfreq 45%

answer

  1. four hooks: post-process, resolve param, afterEach, afterAll
  2. MockKAnnotations.init on the fresh instance
  3. finish(): unmockkAll → checks → clearAllMocks in finally
  4. PER_METHOD cleans per test, PER_CLASS cleans once
  5. KeepMocks / RequireParallelTesting suppress cleanup

basics

~20 s

It initialises annotated mock properties on each new test instance, resolves MockK-annotated test parameters, and afterwards runs unmockkAll() plus clearAllMocks() — after every test method, or after the whole class when the instance lifecycle is PER_CLASS.

solid answer

~40 s

`io.mockk.junit5.MockKExtension` hooks four points: - **Test-instance post-processing** — it calls `MockKAnnotations.init(testInstance)`, filling `@MockK`, `@RelaxedMockK`, `@SpyK` and `@InjectMockKs` properties. With the default per-method instance lifecycle that happens for every test, so each test gets fresh mocks. - **Parameter resolution** — a test parameter annotated `@MockK` / `@RelaxedMockK` is created and handed in. - **After each test** (default lifecycle) or **after the class** (`@TestInstance(PER_CLASS)`) it runs its cleanup: `unmockkAll()`, then the optional strictness checks, then `clearAllMocks()` in a `finally`. `unmockkAll()` matters most: it undoes process-global patching from `mockkObject`/`mockkStatic`/`mockkConstructor` so it cannot leak into other classes. Cleanup can be suppressed with `@MockKExtension.KeepMocks` or `@MockKExtension.RequireParallelTesting`. Plain `mockk()`/`every`/`verify` work with no extension at all — the extension is about annotations and teardown.

code

kotlin · 18 lines
kotlin
@ExtendWith(MockKExtension::class)
class OrderServiceTest {

    @MockK
    lateinit var repository: OrderRepository

    @RelaxedMockK
    lateinit var auditLog: AuditLog

    @Test
    fun `places order`() {
        val service = OrderService(repository, auditLog)
        every { repository.save(any()) } returns 42L

        assertEquals(42L, service.place(Order("abc")))
        verify { repository.save(any()) }
    }
}

go deeper

for a junior

Be able to say it fills in the @MockK-style properties so you do not call MockKAnnotations.init yourself, and that it cleans up after the test.

for a middle

Name the two halves — setup (init on the fresh test instance, parameter resolution) and teardown (unmockkAll then clearAllMocks) — and tie the freshness of mocks to JUnit's per-method test instance.

for a senior

Lead with the teardown value: unmockkAll undoes process-global patching that would otherwise leak across test classes in the same JVM fork, and explain how the per-class lifecycle moves that boundary.

for a principal

Frame it as suite policy: standardise registration (per-class annotation or auto-detection), forbid hand-rolled teardown, and know which opt-outs exist before anyone turns on parallel execution.

## What MockKExtension is `io.mockk.junit5.MockKExtension` is the JUnit 5 extension shipped with MockK. You register it per class with `@ExtendWith(MockKExtension::class)`, or globally by turning on JUnit's extension auto-detection (`-Djunit.extensions.autodetection.enabled=true`). It implements four Jupiter callback interfaces — test-instance post-processing, parameter resolution, after-each and after-all — and everything it does happens in one of those four hooks. Nothing about `mockk()`, `every { }` or `verify { }` depends on it; those work in a plain test with no extension. The extension exists for **annotation-driven setup** and **automatic teardown**. ## Hook 1 — creating the annotated mocks After JUnit constructs the test instance, the extension calls `MockKAnnotations.init(testInstance)` on it. That one call scans the instance's properties for `@MockK`, `@RelaxedMockK`, `@SpyK` and `@InjectMockKs` and populates them, which is exactly the line you would otherwise write by hand in a `@BeforeEach`. The important consequence is *when* it runs: with JUnit's default per-method instance lifecycle a brand-new test instance is built for every test method, so `init` runs per test and every test starts with freshly created mocks that carry no stubs and no recorded calls. That is why a well-formed MockK + JUnit 5 test usually needs no `clearMocks` calls of its own. If the class is annotated `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` there is a single instance for all methods, `init` runs once, and the *same* mock objects are shared by every test — stubs and recorded calls then accumulate unless you clear them yourself. ## Hook 2 — parameter resolution The extension answers "yes, I can supply this parameter" for parameters carrying a MockK annotation (documented: `@MockK` and `@RelaxedMockK`) and creates the mock on the spot. This lets a mock that only one test needs live in that test's signature instead of as a class property. ## Hooks 3 and 4 — the cleanup, and where it runs The extension registers both an after-each and an after-all callback but performs its cleanup in exactly one of them, chosen by the instance lifecycle: per-method (the default) → cleanup after **each** test; per-class → cleanup after **all** tests in the class. The cleanup routine, in order: 1. `unmockkAll()` — unless suppressed. 2. The optional strictness checks (`confirmVerified()` and/or `checkUnnecessaryStub()`), only if the class opted into them. 3. `clearAllMocks()` in a `finally`, so mock state is reset even when a strictness check just failed the test. `unmockkAll()` is the part you cannot easily replicate by accident. `mockkObject`, `mockkStatic` and `mockkConstructor` patch **process-global** state: an object, a file facade, a constructor. Left in place, that patch survives into every later test class in the same JVM fork and produces failures far from their cause. The extension undoing it at the end of each test (or class) is the main reason to add it even to a class that has no annotated properties. `clearAllMocks()` resets answers, recorded calls and child mocks on every registered mock, so a per-class-lifecycle class does not carry stubs across methods. ## Opting out - `@MockKExtension.KeepMocks` — allowed on a class **or a single test method**, or the configuration parameter `mockk.junit.extension.keepmocks=true`; suppresses `unmockkAll()`/`clearAllMocks()`. - `@MockKExtension.RequireParallelTesting` — class level, or `mockk.junit.extension.requireParallelTesting=true`; also suppresses them, because those global calls are not thread-safe. Both annotations are `@Inherited` and are found recursively through meta-annotations, so you can bundle them into your own composed annotation together with `@ExtendWith(MockKExtension::class)`. ## What it does not do It does not install coroutine test dispatchers, does not change how matchers or verification work, and does not replace anything in JUnit itself. And it does not make manual teardown wrong — it makes it redundant. A `@AfterEach { unmockkAll() }` alongside the extension is harmless duplication, but a reviewer will read it as "the author did not know what the extension does". ## Practical shape The idiomatic class is: `@ExtendWith(MockKExtension::class)` on top, collaborators as `@MockK` properties, the class under test built in the test or via `@InjectMockKs`, no init call, no teardown block.

  • If the class is annotated @TestInstance(TestInstance.Lifecycle.PER_CLASS), when does MockKExtension clean up, and what does that change for you?
    Cleanup moves from after each test to after all tests in the class, because there is a single test instance instead of one per method. The annotated mocks are therefore the same objects for every test, and stubs plus recorded calls accumulate across methods. In that setup you take responsibility for isolation yourself — typically a clearMocks/clearAllMocks call in a @BeforeEach — or you avoid the per-class lifecycle for mock-heavy tests.
  • You already call MockKAnnotations.init(this) in a @BeforeEach. Is adding the extension pointless?
    No. The init call only covers annotated properties; the extension additionally resolves MockK-annotated method and constructor parameters and, more importantly, runs unmockkAll() and clearAllMocks() after the test. Manual init leaves global patching from mockkObject/mockkStatic/mockkConstructor registered for the rest of the JVM run. With the extension in place the manual init line becomes redundant and should be deleted.

saying these in an interview costs you the question

  • Claiming mockk(), every and verify only work when the extension is registered — plain MockK needs no extension at all.
  • Saying the extension cleans up before each test; its cleanup runs after the test (or after the class under the per-class lifecycle).
  • Believing the cleanup still runs per method when the class uses @TestInstance(PER_CLASS).
  • Thinking the extension creates mocks lazily on first use rather than when the test instance is post-processed.
  • Keeping a manual @AfterEach { unmockkAll() } and calling it necessary alongside the extension.

context