skip to content

You are setting the house style for a Kotlin service whose handlers orchestrate several collaborators — some orderings are genuinely contractual, most are incidental. How do you decide, per test, between MockK's plain verify, verifyOrder, verifyAll and verifySequence, and how does the scoping of an exhaustive block affect that decision?

level: principalimportance: nice to knowfreq 25%

answer

  1. two axes: completeness × order
  2. order only when causal, not incidental
  3. exhaustive only where an extra call is a defect
  4. exhaustive block binds the mocks it names
  5. segregate the interface before loosening

basics

~20 s

Decide on two axes: is completeness part of the contract, and is order part of the contract. verify asserts neither, verifyOrder order only, verifyAll completeness only, verifySequence both. Exhaustive blocks bind only the mocks they name, so scope them narrowly.

solid answer

~50 s

I decide per collaborator, not per test, on two independent questions. **Is order contractual?** Only when the sequence carries meaning the return values cannot: write inside the transaction, open before write before close, configure before `build()`. Incidental order — two independent reads, a metrics call — is never asserted. **Is completeness contractual?** Only when an extra call is itself a defect: a payment gateway, an outbound port with cost or side effects, a protocol driver. On general-purpose interfaces it just taxes future changes. That gives `verify` for the common case, `verifyOrder` for causal chains, `verifyAll` where the set of interactions matters, and `verifySequence` reserved for genuine protocols. Scoping is the lever people miss: `verifyAll` and `verifySequence` are exhaustive only over the mocks **named inside the block**. So I keep exhaustive blocks to one narrow port and verify the chatty collaborators separately, rather than loosening the mode. Interface segregation does more for this than any DSL choice.

code

kotlin · 9 lines
kotlin
verifyAll {                       // exhaustive over gateway ONLY
    gateway.charge(order.id, order.total)
}
verifyOrder {                      // causal chain, gaps tolerated
    tx.begin()
    repo.save(any())
    tx.commit()
}
verify(exactly = 1) { repo.findById(order.id) }

go deeper

for a junior

Give the simple rule: assert only what the contract says — usually just that a call happened — and reach for order verification only when the sequence itself is the requirement.

for a middle

Present the two axes and map the four functions onto them, with a concrete example of a causal chain worth asserting and an incidental one that is not.

for a senior

Add scoping — exhaustive blocks bind only the mocks they name — and argue interface segregation as the structural fix for noisy strict blocks.

for a principal

Frame it as a suite-level economics decision: which failures teach the team something, how strictness erodes trust in the suite, and where concurrency removes order from the contract entirely.

## Why this is a policy question, not a preference MockK's four verification modes sit on two independent axes — completeness (are unlisted calls allowed?) and order (must matched calls appear in listed order?). `verify` asserts neither, `verifyOrder` asserts order only, `verifyAll` completeness only, `verifySequence` both. Choosing badly does not produce a wrong test; it produces a test that fails for reasons unrelated to the behaviour it names. Over a suite of thousands, that is what teaches engineers to edit assertions without reading them, which is how interaction tests stop catching anything. ## Axis one: is the order contractual? Order deserves an assertion when the sequence carries a guarantee that no return value expresses: - **Causal/transactional**: `begin` → `save` → `commit`. If save escapes the transaction, nothing about the returned value changes but the system is broken. - **Resource protocols**: open before write before close; acquire before release. - **Externally observable order**: audit written before the response is emitted; idempotency key persisted before the outbound call. Order does *not* deserve an assertion when it is an artefact of how the method happens to be written — two independent reads, a cache lookup relative to a metrics counter, arguments assembled in some sequence. Asserting incidental order buys nothing and breaks on every harmless refactor. ## Axis two: is completeness contractual? Exhaustiveness earns its cost when an *extra* call is a defect in itself: - Outbound ports with cost or side effects: sending an email twice, charging a card, publishing an event. - N+1 protection on a repository port for a hot path. - Protocol drivers where an unexpected frame is a bug. It is a poor default on wide, general-purpose interfaces, on collaborators that also carry telemetry, and anywhere relaxed mocks are in play — a relaxed mock answers calls nobody thought about (accessors, `size`, id getters), and each becomes a record an exhaustive block demands you cover. ## Scoping: the lever most people miss Exhaustiveness in MockK is scoped to the mocks **referenced inside the block**, not to the test. This is what makes strict verification survivable in a multi-collaborator handler: ```kotlin // Strong statement about the outbound port only verifyAll { gateway.charge(order.id, order.total) } // Everything else verified loosely, on purpose verify { repo.save(any()) } verifyOrder { tx.begin(); repo.save(any()); tx.commit() } ``` The `verifyAll` block freezes the interaction with `gateway` and says nothing about `repo`, `tx` or the metrics collaborator. Mixing several mocks into one exhaustive block is the mistake: it silently freezes all of them, and then the first unrelated call anywhere breaks a test whose name mentions payments. ## Design beats DSL When a collaborator is too chatty to verify exhaustively, the honest fix is usually interface segregation: the domain port and the telemetry port become different types, so the telemetry traffic lands on a mock the strict block never names. That preserves the strong guarantee where it matters and removes the churn, without reaching for record-muting helpers. Muting should be a last resort and always carry a comment, because a call excluded from the record is invisible to *every* later assertion, not just the strict one. ## Nondeterminism ends the discussion If the collaborators are invoked from several threads or concurrently launched coroutines, the recorded order is not a contract at all — it is a race outcome. Asserting it produces a flaky suite. There the right answers are to assert the invariant that actually holds (a call happened, a count, a causal pair enforced by the code itself) or to make the production code's ordering explicit and test that instead. ## Cost and diagnosability One more criterion: which mode gives the best failure message for the mistake you are guarding against? A `verifySequence` failure on a wide mock produces a wall of recorded calls and a reader who cannot tell which difference mattered. A focused `verifyOrder` of three calls fails with a message that names the broken causality. Prefer the mode whose failure explains itself. ## The house rule I would write down 1. Default to plain `verify` with explicit counts where the count matters. 2. Add `verifyOrder` only for named causal chains; list the minimum number of calls that expresses the causality. 3. Use `verifyAll` — never `verifySequence` — when completeness is the point but order is not, and scope it to a single narrow port per block. 4. Reserve `verifySequence` for protocol-shaped collaborators, and treat a failure there as a design question, not an assertion to update. 5. Never assert order across concurrently invoked collaborators. 6. If a strict block is noisy, split the interface before you loosen the mode.

  • A teammate proposes verifySequence as the default for all interaction tests, arguing it catches the most bugs. What is your counter-argument?
    It catches the most *changes*, not the most bugs. Because it asserts completeness as well as order, every future call on the collaborator — telemetry, an extra read, a new field lookup — fails tests that never intended to specify that. The signal-to-noise collapse is the real cost: people stop reading failures and start editing blocks. I would keep it for protocol-shaped ports where an unexpected call genuinely is a defect.
  • How does concurrency change your ordering policy?
    It removes ordering from the contract. If two collaborators are invoked from different threads or independently launched coroutines, the recorded order is a race outcome, so asserting it produces flakiness that will eventually be muted or retried away. I assert what actually holds — that the calls happened, their counts, or a causal pair the production code enforces by construction — and if the ordering truly matters I make the code enforce it explicitly and test that mechanism.

saying these in an interview costs you the question

  • Making verifySequence the default because "stricter is safer"
  • Putting several mocks in one exhaustive block and not realising it freezes all of them
  • Asserting incidental orderings that only reflect how the method happens to be written
  • Muting calls out of the record as the first response to a noisy strict block
  • Asserting order across concurrently invoked collaborators and calling the resulting flakiness a MockK problem

context