skip to content

Your team's interaction tests using verifySequence and confirmVerified keep breaking on benign refactors. How would you diagnose this and rework the verification strategy while keeping meaningful coverage?

level: principalimportance: nice to knowfreq 25%

answer

  1. brittleness = asserting trace, not contract
  2. downgrade verifySequence to verifyOrder / verify(exactly)
  3. excludeRecords to strip noise before confirmVerified
  4. prefer state assertions over interaction assertions
  5. reserve sequence lock-down for real protocols

basics

~20 s

Over-strict verifiers couple tests to incidental call order and noise. Reserve verifySequence/confirmVerified for true protocols; elsewhere use verify with counts or verifyOrder for the calls that matter, and excludeRecords to drop noise. Prefer state assertions over interaction assertions when possible.

solid answer

~40 s

The brittleness comes from asserting more than the contract: verifySequence locks the exhaustive, ordered call list, and confirmVerified forbids any unverified call, so any new logging, metric, or reordered-but-equivalent call fails the test. Diagnose by asking which assertions encode real behavioral contracts versus incidental implementation detail. Rework: (1) downgrade most verifySequence to verifyOrder (relative happens-before) or plain verify(exactly = n); (2) keep verifySequence only where call order is genuinely part of the contract (e.g., begin/commit/close); (3) use excludeRecords to strip diagnostic noise before confirmVerified; (4) prefer verifying observable state/return values over interactions where the collaborator's effect is testable directly. The principle: assert the contract, not the implementation trace. Pair with mockk's checkUnnecessaryStub to remove dead stubs that also signal over-specification.

code

kotlin · 5 lines
kotlin
// reworked: assert contract, ignore noise
excludeRecords { repo.metric() }
verifyOrder { repo.begin(); repo.commit() }   // real ordering contract
verify(exactly = 1) { repo.save(any()) }       // count that matters
confirmVerified(repo)                          // stable now

go deeper

for a junior

Recognizes that strict verifiers can break on harmless changes and that looser verify modes exist.

for a middle

Can swap verifySequence for verifyOrder/verify(exactly) and apply excludeRecords to reduce false failures.

for a senior

Diagnoses each assertion as contract vs incidental and reworks the suite along a coupling ladder.

for a principal

Establishes team-wide policy: test the contract not the trace, reserve sequence lock-down for real protocols, and use exclusion/stub-hygiene to keep interaction tests durable.

## Why the tests are brittle Interaction tests assert **how** code talks to collaborators. The strictest MockK modes assert the most: - `verifySequence { ... }` — the **complete, ordered** list of calls; any extra, missing, or reordered call fails. - `confirmVerified(mock)` — **no** unverified call may remain on the mock. When production code gains a benign call (a new `logger.debug`, a metrics tick, a reordered-but-commutative pair), these modes fail even though behavior is unchanged. The test is coupled to the **implementation trace**, not the **contract**. ## Diagnosis For each failing assertion, ask: *does this encode a real behavioral guarantee, or an incidental implementation detail?* - Order of `begin → commit → close` on a transaction → **real contract**; keep ordered verification. - Order of two independent cache reads, or the presence of a log line → **incidental**; don't pin it. ## Reworking the strategy ```kotlin // BEFORE: brittle — pins the entire trace and order verifySequence { repo.begin(); repo.save(any()); repo.metric(); repo.commit() } confirmVerified(repo) // AFTER: assert only the load-bearing contract excludeRecords { repo.metric() } // diagnostic noise verifyOrder { repo.begin(); repo.commit() } // the real happens-before verify(exactly = 1) { repo.save(any()) } confirmVerified(repo) // now stable: metric excluded ``` ### Decision ladder (least to most coupling) 1. **State assertion** — check the return value or resulting state directly; no interaction verify at all. Most robust. 2. **`verify(exactly = n)`** — the call happened the right number of times; order-agnostic. 3. **`verifyOrder`** — a specific happens-before relationship that is part of the contract. 4. **`verifySequence` + `confirmVerified`** — full protocol lock-down; reserve for state-machine collaborators where order **is** the contract. ### Supporting tactics - `excludeRecords { ... }` removes noise (logging, metrics) from the recording so `confirmVerified`/`verifySequence` ignore it. - `checkUnnecessaryStub(mock)` flags `every` stubs that were never invoked — over-stubbing often accompanies over-verification; pruning both reduces brittleness. - Prefer **relaxed mocks** for collaborators whose return values are irrelevant, then verify only the meaningful interactions. ## The governing principle Test the **contract**, not the **trace**. Every assertion should fail only when behavior the caller depends on actually changes. Strict ordering and exhaustiveness are powerful but should be a deliberate choice for genuine protocols — not the default — so the suite stays a refactoring aid rather than a refactoring tax.

  • When is verifySequence genuinely the right tool despite its brittleness?
    When call order is part of the contract — transaction begin/commit/close, protocol handshakes, or state machines where reordering would be a real bug.
  • How does over-stubbing relate to over-verification?
    Both over-specify. checkUnnecessaryStub surfaces stubs never called; pruning them and loosening verification together cut coupling to implementation detail.

Strict sequence checks are like grading an essay on the exact route the writer's pen took; you should grade the finished argument, not the pen strokes.

saying these in an interview costs you the question

  • Defending verifySequence everywhere as 'more thorough'
  • Suppressing failures by re-pinning the new trace instead of asking what the contract is
  • Never considering state assertions as an alternative to interaction verification
  • Ignoring excludeRecords and instead deleting useful logging to satisfy tests

context