skip to content

What do MockK's @MockKExtension.ConfirmVerification and @MockKExtension.CheckUnnecessaryStub class annotations make happen, when do they run, and what is the cost of switching them on across a whole test suite?

level: seniorimportance: should knowfreq 25%

answer

  1. ConfirmVerification → global confirmVerified() at cleanup
  2. CheckUnnecessaryStub → dead stubs fail the test
  3. class-level, @Inherited, meta-annotation aware
  4. config params: mockk.junit.extension.confirmverification / .checkUnnecessaryStub
  5. clearAllMocks still runs in finally; property has no per-class opt-out

basics

~20 s

They make MockKExtension run a global confirmVerified() and checkUnnecessaryStub() at the end of the test, failing it when a recorded call went unverified or a stub was never used. Both are class-level, and both can be turned on suite-wide by a JUnit configuration parameter.

solid answer

~50 s

Both are opt-in strictness switches on `io.mockk.junit5.MockKExtension`, applied to a **test class** (they are `@Inherited` and found through meta-annotations, so you can bundle them into your own annotation): - `@MockKExtension.ConfirmVerification` → the extension calls global `confirmVerified()` in its cleanup: the test fails if any recorded call on any mock was not covered by a `verify`. - `@MockKExtension.CheckUnnecessaryStub` → it calls `checkUnnecessaryStub()`: the test fails if a stub you recorded was never actually invoked. They run in the same cleanup as the teardown — after each test with the default lifecycle, after the class under `PER_CLASS` — after `unmockkAll()` and before the `finally` `clearAllMocks()`, so state is still reset when they fail. Suite-wide equivalents are the configuration parameters `mockk.junit.extension.confirmverification` and `mockk.junit.extension.checkUnnecessaryStub`. The cost: every test becomes an interaction test. Shared `@BeforeEach` stubs that only some tests use now fail; incidental calls on relaxed mocks must be verified or excluded. Enabling per class is the safer default.

code

kotlin · 18 lines
kotlin
@ExtendWith(MockKExtension::class)
@MockKExtension.ConfirmVerification
@MockKExtension.CheckUnnecessaryStub
class PaymentGatewayTest {

    @MockK
    lateinit var http: HttpClient

    @Test
    fun `charges once`() {
        every { http.post("/charge", any()) } returns Response(200)
        every { http.post("/refund", any()) } returns Response(200) // never used -> fails

        PaymentGateway(http).charge(10)

        verify { http.post("/charge", any()) }
    }
}

go deeper

for a junior

Know that the two annotations exist and make the extension fail tests with unverified calls or unused stubs.

for a middle

Say which is which, that they are class-level, and that they run in the extension's end-of-test cleanup rather than inside the test body.

for a senior

Cover the enablement paths (annotation OR configuration parameter, no per-class opt-out for the property), the cleanup ordering, and the concrete failure patterns — relaxed-mock chatter, shared @BeforeEach stubs.

for a principal

Answer as policy: adopt CheckUnnecessaryStub broadly, ConfirmVerification only where the interaction is the contract, and weigh suite-wide brittleness against the defects the checks actually catch.

## The two switches MockK's JUnit 5 extension carries a small family of nested annotations. Two of them turn the extension from a setup/teardown helper into a **strictness enforcer**: - `@MockKExtension.ConfirmVerification` — at cleanup time the extension invokes MockK's global `confirmVerified()` (no arguments, i.e. every registered mock). If a mock recorded a call that no `verify`/`coVerify` block matched, the test fails. - `@MockKExtension.CheckUnnecessaryStub` — at the same point it invokes `checkUnnecessaryStub()`, which fails the test when a stub was recorded but never exercised. One asks "did you look at everything that happened?", the other asks "did everything you set up actually matter?". They attack opposite ends of the same sloppiness. ## Where and when they run They are not separate callbacks — they live inside the extension's normal cleanup routine, which runs **after each test method** under JUnit's default per-method instance lifecycle, or **after the whole class** when the class is annotated with the per-class lifecycle. The order inside that routine matters and is worth knowing: 1. `unmockkAll()` (unless suppressed), 2. `confirmVerified()` if `ConfirmVerification` is on, 3. `checkUnnecessaryStub()` if `CheckUnnecessaryStub` is on, 4. `clearAllMocks()` in a `finally`. The `finally` is deliberate: a failing strictness check must not leave the next test staring at a polluted mock registry. Note also the per-class consequence — with the per-class lifecycle the checks see the *accumulated* interactions of every test in the class, which is almost never what you want; these switches fit the default per-method lifecycle. ## How they are enabled Both annotations target a **class** (unlike `KeepMocks`, which also works on a single method). Both are `@Inherited`, and the extension searches annotations recursively through meta-annotations, so a project can define one custom annotation that carries `@ExtendWith(MockKExtension::class)` plus the strictness switches and apply that everywhere. Both also have a suite-wide equivalent as a JUnit Platform configuration parameter — `mockk.junit.extension.confirmverification` and `mockk.junit.extension.checkUnnecessaryStub` (mind the casing of the second) — supplied through the platform's configuration file or as a system property. The extension enables a check when **either** the class annotation **or** the configuration parameter says so. That OR is the important operational detail: once the property is set globally there is no per-class opt-*out* annotation. A single legacy test class that cannot satisfy the check will hold the whole property hostage, which is the main argument for adopting the annotations class by class first and only later considering the global switch. ## What it costs you **Every test becomes an interaction test.** Without `ConfirmVerification`, a test asserts the outcome it cares about and ignores the rest; with it, any call the subject makes on any mock is a potential failure unless verified. Refactors that add a legitimate collaborator call now break tests that had nothing to do with that behaviour — the classic brittleness complaint about strict interaction testing, applied automatically to your whole class. **Relaxed mocks get noisy.** A relaxed mock answers everything, so subjects freely call logging, metrics and audit sinks on it. Those calls are recorded and therefore must be verified or explicitly excluded from the record. **Shared setup stubbing breaks.** A `@BeforeEach` that stubs five collaborator methods for the convenience of the whole class will trip `CheckUnnecessaryStub` in every test that only needs three of them. The intended fix is to move stubbing into the tests that need it, which is genuinely better practice but is a real migration cost on an existing suite. **Failures land in teardown.** The stack you see points at the extension's cleanup, not at the line that stubbed or called. The message content is what identifies the offending mock and call, so read it rather than the location. ## How to adopt them sensibly Start with `CheckUnnecessaryStub` — it flags dead setup, which is nearly always a genuine defect and rarely encodes a design opinion. Add `ConfirmVerification` selectively, on classes where the interaction *is* the contract: an event publisher, an outbound gateway, a retry policy. Leave it off for tests whose subject legitimately chatters at relaxed sinks, or clean the record for that noise. And treat any global configuration parameter as a decision for the whole repository, because it cannot be waived per class.

  • A test using @MockKExtension.ConfirmVerification fails even though every call you care about is verified. What are the usual causes?
    Almost always incidental interactions you did not think of: a relaxed mock absorbing logging, metrics or audit calls, or a collaborator method the subject calls for bookkeeping. The record contains every call, not just the interesting ones. Either verify those calls explicitly, keep them out of the recorded interactions, or decide this class does not need the switch.
  • Would you enable these repo-wide with the configuration parameters, or per class?
    Per class first. The extension ORs the class annotation with the configuration parameter, so a global property cannot be waived by an individual class — one class that legitimately cannot satisfy the check blocks the setting for everyone. Adopting per class lets you migrate incrementally and keeps the choice visible in the test file itself.

saying these in an interview costs you the question

  • Thinking the annotations work on individual test methods; they target the class.
  • Assuming a class annotation can switch the check off when the global configuration parameter turns it on — the two are ORed.
  • Believing a failed strictness check skips cleanup; clearAllMocks still runs in a finally.
  • Treating CheckUnnecessaryStub failures as false positives instead of dead setup to delete or relocate.
  • Turning both on suite-wide as a 'quality' move without accounting for relaxed-mock noise and shared @BeforeEach stubbing.

context