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?
answer
- brittleness = asserting trace, not contract
- downgrade verifySequence to verifyOrder / verify(exactly)
- excludeRecords to strip noise before confirmVerified
- prefer state assertions over interaction assertions
- reserve sequence lock-down for real protocols
basics
~20 sOver-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 sThe 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// 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 nowgo deeper
Recognizes that strict verifiers can break on harmless changes and that looser verify modes exist.
Can swap verifySequence for verifyOrder/verify(exactly) and apply excludeRecords to reduce false failures.
Diagnoses each assertion as contract vs incidental and reworks the suite along a coupling ladder.
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