Besides checking that calls were verified, MockK can flag stubs that were never used, via checkUnnecessaryStub. What condition makes it throw, what class of bug does it catch that confirmVerified structurally cannot, and when does it produce false alarms?
answer
- answer table vs call record — mirror-image checks
- throws when a stub was never matched
- catches dead stubs and wrong-matcher stubs
- relaxed mock hides mis-matched stubs; this exposes them
- shared @BeforeEach stubbing ⇒ false alarms
basics
~20 scheckUnnecessaryStub(mock) throws if the mock has stubs that no actual call ever matched. It inspects the answer table, so it catches dead or mis-matched stubs — the opposite blind spot from confirmVerified, which only inspects recorded calls. Shared setup stubbing more than a test needs causes false alarms.
solid answer
~50 sA MockK mock keeps two books: the **answer table** written by `every { }`, and the **call record** of real invocations. `confirmVerified` reads the record and complains about calls nobody verified. `checkUnnecessaryStub(mock)` reads the answer table and complains about stubs nobody called — it throws when at least one registered stub was never matched by an actual invocation. The bug class it catches is dead or wrong stubbing: a stub left behind after a refactor, a copy-pasted setup, or — the valuable case — a stub whose matcher does not match the real arguments. On a relaxed mock a mis-matched stub is silent, because the call falls through to the relaxed default and the test may still pass for the wrong reason; `checkUnnecessaryStub` turns that into a failure. False alarms come from shared setup: a `@BeforeEach` that stubs the union of what all tests need means most tests leave stubs unused. The fix is per-test stubbing, not disabling the check.
code
kotlin · 6 linesval repo = mockk<OrderRepo>(relaxed = true)
every { repo.findById(1) } returns order // code actually calls findById(2)
service.handle(cmd) // silently gets the relaxed default
checkUnnecessaryStub(repo) // AssertionError: stub never matchedgo deeper
Say it fails the test when a stub set up with every was never actually called, which usually means the stub is dead or the arguments do not match.
Contrast the two books explicitly — answer table versus call record — and give the refactor-residue and copy-paste-setup examples.
Lead with the mis-matched-matcher case on relaxed mocks, where a test passes for the wrong reason, and explain shared setup as the source of false alarms.
Position it as the cheap half of exhaustive checking to enable broadly across a suite, with confirmVerified reserved for narrow ports where an extra call is a real defect.
## Two books, two blind spots MockK maintains, per mock, an **answer table** (what `every { }` registered) and a **call record** (what was actually invoked). Exhaustive verification tools exist for both sides: | Tool | Reads | Complains about | |---|---|---| | `confirmVerified(mock)` | call record | invocations that no `verify` matched | | `checkUnnecessaryStub(mock)` | answer table | stubs that no invocation matched | They are structurally blind to each other's territory. A test can be fully exhaustive on verification and still be full of dead stubs, because stubbing writes nothing to the record — `confirmVerified` has nothing to see. Symmetrically, `checkUnnecessaryStub` says nothing about calls that happened but went unverified. ## The failure condition `checkUnnecessaryStub(vararg mocks)` throws an `AssertionError` when any of the given mocks carries a stub that was never matched by an actual call. "Matched" is the same matcher machinery used everywhere else, which is what makes the check interesting: a stub registered as `every { repo.findById(1) } returns order` is *not* satisfied by a real call to `findById(2)`. ## The bug class it catches Three recurring cases: 1. **Refactor residue.** The code path that used the collaborator moved or disappeared, but the stub stayed. The test still passes and quietly asserts less than its author thought. Dead stubs are dead code with a false sense of coverage attached. 2. **Over-stubbed setup.** Copy-paste test setups grow stubs nobody needs, and readers can no longer tell which stubbing is load-bearing for a given case. 3. **The valuable one: a matcher that does not match.** You stub `findById(1)` and the code actually calls `findById(2)`. On a strict (non-relaxed) mock, the unstubbed call fails loudly anyway. On a **relaxed** mock it does not: the call falls through to the relaxed default — `null`, `0`, an empty collection, a chained child mock — and the test may still go green for entirely the wrong reason. `checkUnnecessaryStub` is the tool that turns that silence into a failure, which is why it pairs naturally with relaxed mocks: relaxation removes the loud failure, this check restores a quieter one. ## False alarms and how to handle them The common false positive is **shared setup**. A `@BeforeEach` (or an `init` block) that stubs everything any test in the class might need guarantees that most tests leave several stubs unused, so the check fires on tests that are perfectly correct. Responses, best first: - Move stubbing into each test, stubbing only what that test exercises. This is the right answer almost always — it also makes each test readable on its own. - Keep in shared setup only the stubs every test genuinely needs. - Apply the check selectively: it takes explicit mocks, so you can check the mocks you care about and leave a deliberately over-stubbed fixture out. A second, subtler case: a stub that exists to *prevent* a call from doing something (defensive stubbing on a mock the test hopes never to touch). That is better expressed as an explicit `verify(exactly = 0) { ... }` or by leaving the mock strict so an unexpected call throws. ## Running it automatically Calling `checkUnnecessaryStub(mock)` by hand at the end of a test is the explicit form. MockK's JUnit 5 extension also exposes it as an opt-in that applies the check after each test, via the `@MockKExtension.CheckUnnecessaryStub` annotation on the test class — the extension mechanics themselves are a separate topic, but it is worth knowing the check does not have to be written out in every test. ## How strict should you be? Dead-stub detection is cheap and its failures are almost always genuine (the exception being shared setup, which is itself a smell), so it scales better across a suite than exhaustive *verification* does. A common house rule: enable stub checking broadly, and reserve `confirmVerified` for the narrow ports where an unexpected call is a real defect. ## Version note This is current MockK (1.13.x) behaviour. `checkUnnecessaryStub` and the extension option are present in recent releases; on much older 1.9/1.10-era versions you should confirm availability against the version on your classpath rather than assume it.
- Why is checkUnnecessaryStub especially valuable when a test uses relaxed mocks?A strict mock throws when an unstubbed call arrives, so a mis-matched stub announces itself immediately. A relaxed mock answers instead with a default value, so the call succeeds and the test can pass for the wrong reason while your stub sits unused. checkUnnecessaryStub inspects the answer table and reports that unused stub, restoring the signal relaxation removed.
- Your class stubs everything in @BeforeEach and the check now fails in most tests. What do you do?Move stubbing into the individual tests so each one stubs only what it exercises, keeping in shared setup only what every test genuinely needs. The failures are telling you the setup asserts things most tests do not use, which also makes them harder to read in isolation. Disabling the check would preserve the readability problem and lose the dead-stub signal.
saying these in an interview costs you the question
- Thinking confirmVerified would also catch an unused stub — it never inspects the answer table
- Assuming a stub counts as used merely because the method was called, regardless of whether the matcher matched
- Treating every failure as noise and turning the check off, rather than fixing shared over-stubbing
- Believing it detects unverified calls, confusing it with confirmVerified
- Claiming a relaxed mock makes stub problems visible — relaxation is exactly what hides them