Your team is deciding whether to turn MockK's exhaustive checks — confirmVerified on mocks and unnecessary-stub detection — on by default across a large Kotlin test suite. How do you reason about where each pays for itself, and what does heavy use of relaxed mocks do to that calculation?
answer
- stub check = answer table, cheap, broad default
- confirmVerified = call record, freezes interaction, scoped
- strict on outbound ports with cost/side effects
- relaxed mocks fight blanket confirmVerified
- exhaustiveness is per-mock ⇒ a port-design decision
basics
~20 sTreat them separately. Dead-stub detection is cheap and its failures are nearly always real, so enable it broadly. confirmVerified freezes the whole interaction with a mock, so scope it to narrow ports where an unexpected call is itself a defect. Relaxed mocks generate incidental records that make blanket confirmVerified noisy.
solid answer
~50 sThese are two different bets and deserve different defaults. **Unnecessary-stub detection** inspects the answer table and fails on stubs no call ever matched. Its failures are almost always genuine — dead stubs, copy-pasted setups, or a matcher that silently missed. It is cheap to satisfy and it catches tests that pass for the wrong reason, so I enable it broadly. The one recurring false alarm, a shared setup stubbing the union of what all tests need, is itself a smell worth fixing. **confirmVerified** inspects the call record and fails on any invocation nothing verified. That freezes the complete interaction with a mock, so every future call on that collaborator becomes a test edit. Worth it on outbound ports where an extra call has cost or side effects; a tax elsewhere. Relaxed mocks tilt the second bet further: relaxation exists so incidental calls just work, but each is still recorded, so blanket `confirmVerified` fights the very convenience the team chose. Narrow the ports rather than muting records.
code
kotlin · 7 linesservice.place(order)
verify(exactly = 1) { gateway.charge(order.id, order.total) }
confirmVerified(gateway) // only this port is frozen
verify { repo.save(any()) } // deliberately non-exhaustive
checkUnnecessaryStub(repo, gateway)go deeper
Keep it simple: unused-stub detection is cheap and worth having on; freezing the complete set of calls on a mock is strong and should be used where an extra call would be a real problem.
Separate the two checks by what they inspect, and give concrete examples of where each pays off and where it becomes noise.
Add the relaxed-mock tension in both directions — it raises the value of stub detection and lowers the value of blanket confirmVerified — and describe the curated-port approach.
Argue the suite-level economics: which failures teach the team something, how noise erodes trust in strict assertions, and why interface segregation is the real lever behind both checks.
## Two checks, two economics MockK gives each mock an **answer table** (stubs from `every { }`) and a **call record** (real invocations). The exhaustive tools inspect one each: `checkUnnecessaryStub` fails on stubs nothing matched, `confirmVerified` fails on calls nothing verified. Because they guard different structures, they have genuinely different cost/benefit profiles, and rolling them out as a single "strict mode" policy is the first mistake. ## The stub check: cheap, high-signal, enable broadly What does a failure mean? Either a stub is dead — the code path moved and nobody cleaned up — or a stub's matcher never matched the real call. The second is the valuable one. On a strict mock a mis-matched stub announces itself when the unstubbed call throws; on a relaxed mock it does not, because the call quietly takes the relaxed default and the test can go green while asserting nothing meaningful. So the stub check restores exactly the signal that relaxation removes. Cost to satisfy: stub only what the test exercises. That is a property you want anyway, since a test whose setup stubs things it never uses is harder to read. The one systemic false alarm is a shared `@BeforeEach` stubbing the union of the class's needs — and the right response is to push stubbing down into the tests, not to switch off the check. Because it takes explicit mocks (and MockK's JUnit 5 extension exposes it as an opt-in applied after each test), you can still exempt a deliberately shared fixture. ## confirmVerified: powerful, scoped, not a default `confirmVerified(mock)` says "this is the complete list of interactions with this collaborator". Every future call on that mock — a new read, a telemetry counter, an extra field lookup — becomes a failing test somewhere. That is precisely right when an unexpected call is itself a defect: - Payment gateways, email senders, event publishers: an extra call costs money or duplicates an effect. - N+1 protection on a repository port in a hot path. - Protocol-shaped drivers. It is a poor default on wide service interfaces, on collaborators that carry telemetry alongside domain operations, and on anything shared across many tests, where the noise arrives on changes nobody considers meaningful. The failure mode is cultural rather than technical: when a suite fails for reasons the team judges irrelevant, people stop reading failures and start editing assertions, and then the strict checks that *did* matter get edited away too. A useful implementation detail for policy: exhaustiveness is per-mock and opt-in per call site — `confirmVerified(gateway)` binds only that mock, and `verifyAll`/`verifySequence` bind only the mocks named inside their blocks. So the decision is not suite-wide strictness versus none; it is *which ports* get frozen. Encode that in the port design: strict on the small, deliberately-shaped outbound interfaces, loose on everything else. ## What relaxed mocks do to the calculation A relaxed mock answers anything, which is why teams like it — no ceremony for calls that do not matter. But those calls are still recorded, so relaxation and blanket `confirmVerified` are in direct tension: one says "incidental calls need no attention", the other says "every call must be accounted for". A team that leans on relaxation and then turns on exhaustive verification everywhere will spend its time writing verifications for accessors. There are three honest resolutions, in order of preference: 1. **Narrow the port.** Split domain operations from telemetry so the noisy traffic lands on a mock the strict assertion never names. This keeps both the strong guarantee and the ability to assert on the telemetry when you want to. 2. **Scope the strictness.** Pass only the ports that deserve it to `confirmVerified`. 3. **Exclude specific incidental members from the record.** Effective, but remember an excluded call is invisible to *every* verification afterwards — you are deleting evidence, not filtering a report — so it should be commented and never applied to a member with cost or side effects. Meanwhile relaxation *increases* the value of the stub check, for the reason above: it is the only cheap guard against a stub whose matcher silently missed. ## The policy I would write 1. Unnecessary-stub detection on by default; fix shared over-stubbing rather than exempting classes. 2. `confirmVerified` on a curated list of outbound ports where an extra call is a defect; never as a blanket rule. 3. Prefer narrow, segregated ports over record exclusions; exclusions require a comment. 4. Avoid mocks shared across tests when either check is in play — accumulated records and registered exclusions make results order-dependent. 5. Revisit any strict assertion that has been "updated to make it pass" twice; that is the suite telling you the mock is not a good candidate for exhaustiveness. ## What an interviewer is listening for Not a rule, but a separation: you know the two checks guard different structures, you can name the failure modes each produces, you understand that exhaustiveness is per-mock and therefore a design decision about ports, and you can articulate why relaxation and blanket exhaustiveness are philosophically opposed.
- Someone argues confirmVerified should be applied to every mock so nothing slips through. What is your response?It does not catch more bugs, it catches more changes. Freezing every collaborator's complete interaction means unrelated work — a log line, a new read, a metrics counter — fails tests that never meant to specify that. The suite then trains people to edit assertions without reading them, which erodes the strict checks that genuinely mattered. I would apply it to a curated set of outbound ports and rely on narrow interfaces to make those sets small.
- A team uses relaxed mocks everywhere and wants stronger interaction guarantees. What do you suggest first?Change the ports before changing the checks. Relaxation is usually a symptom of wide interfaces with lots of incidental members; splitting the domain operations from telemetry and accessors gives small mocks that need little relaxation and can be verified exhaustively without noise. Alongside that, enable unnecessary-stub detection immediately, because relaxation is exactly what hides a stub whose matcher never matched.
saying these in an interview costs you the question
- Rolling both checks out as one suite-wide "strict mode" without separating their economics
- Claiming confirmVerified catches more bugs, when what it mostly catches is unrelated change
- Turning off stub detection because a shared @BeforeEach makes it noisy
- Treating record exclusion as a routine way to quiet exhaustive checks
- Missing that exhaustiveness is per-mock, and therefore a decision about port design rather than a global switch