In a large test suite using Mockito, argument assertions can live inside the verification as custom predicate matchers, or be pulled out and asserted separately on the recorded value. How do you decide which, and what does each choice cost in diagnosability and coupling?
answer
- matchers discriminate, assertions explain
- only matchers work in stubbing / InOrder
- boolean result = no field-level diff
- 3+ uses -> named matcher with toString()
- triage cost is the real metric; add strict stubs
basics
~20 sUse a predicate matcher when the condition must select which invocation or which stubbed behaviour applies. Pull the assertion out when you need a readable explanation of what was wrong. Matchers discriminate, assertions explain - and matchers also work in stubbing, where extracted assertions cannot.
solid answer
~50 sI split it by job. A matcher participates in matching: it decides which recorded invocation counts, or which stub answers a call. That capability is unique to matchers and is the only reason to accept their weaknesses - a boolean result, a description you must write by hand, and a predicate that runs inside Mockito's machinery so it cannot assert or have side effects. Extracted-value assertions do the opposite: they run after the fact with the full power of the assertion library, so failures name the offending field and print a diff, but they cannot influence stubbing and get awkward when the same method was called many times. My rule for a suite: verification expresses which call happened, assertions express what was in it. Keep predicates small and named when reused, and treat a large predicate inside a verify as a smell - it is an assertion wearing the wrong hat, and it will fail one day with no explanation.
go deeper
Know that both options exist and that a predicate inside verify gives a yes/no failure while a separate assertion explains what differed.
Add the rule of thumb - matchers when matching must be affected, assertions when the message matters - with a concrete example of each.
Argue from failure diagnosability and give the reuse threshold at which an inline lambda becomes a named matcher.
Make it a suite-wide policy: triage cost as the metric, an explicit stance on how much of an argument a test may pin, and strict stubbing as the backstop for argument-dependent stubs.
## Two mechanisms, two jobs A custom matcher passed to argThat is evaluated by Mockito while matching invocations. It can therefore do things nothing else can: make when(...) return different answers for different arguments, disambiguate which of several recorded calls a verification refers to, and constrain a call inside verify(mock, times(2)) or an InOrder sequence. An extracted-value assertion runs afterwards on the actual argument and can use the full assertion library - recursive comparison, field exclusions, soft assertions, printed diffs. ## What each costs Predicate matchers cost diagnosability. The result is a boolean, so a failure can only say the call did not match; the wanted line is whatever toString() you wrote, and if you wrote none it is a synthetic lambda name. They also carry rules: pure, null-safe, non-throwing, possibly evaluated more than once. And they hide the assertion inside a call that also does other work, so a reader has to disentangle 'which call' from 'what was in it'. Extracted assertions cost expressiveness at match time. They cannot make a stub argument-dependent. When a method was called several times you must decide which occurrence you meant, which is exactly the discrimination a matcher does for free. And they run only after the verification has already passed, so a wrong-call-shape failure and a wrong-content failure surface in two different places. ## A decision rule that survives a large suite 1. Does the condition need to affect matching - stubbing behaviour, picking one of several invocations, ordering? If yes, it must be a matcher. 2. Otherwise, is the condition trivially obvious on failure - a single flag, one enum, an id? An inline predicate is fine, and cheaper to read than a two-step extract-and-assert. 3. Otherwise - several fields, numeric tolerance, collection contents - assert on the value outside the verification, where a failure explains itself. 4. Anything appearing in three or more tests becomes a named ArgumentMatcher with a domain name and a toString(), or a shared assertion helper. Duplicated inline predicates are how suites drift. ## Coupling, the other axis Both mechanisms couple the test to the collaborator's call shape; the question is how much of the argument's internals the test pins. Asserting every field of a large DTO makes any construction change break dozens of tests, and teams respond by deleting assertions rather than fixing them. Asserting nothing lets real regressions through. The stable middle is to assert the fields that carry the behaviour under test and be explicit about ignoring the rest - excluded fields in a recursive comparison, or a matcher whose description names precisely what it checks. The description doubles as documentation of what this test claims. ## Failure-mode economics At suite scale, judge these choices by triage cost. A CI failure that reads 'Wanted: repo.save(a PENDING order with total > 100), Actual: save(Order[status=PENDING, total=99.99])' is fixed from the log. A failure that reads 'Wanted: save(Test$$Lambda$14@6d06d69c)' requires a local reproduction and a debugger. Multiply by the number of failures a large suite produces during a refactor and the policy pays for itself. That is also why assertions must never be embedded inside matchers - they promise a good message and deliver a confusing one, because Mockito may evaluate the matcher repeatedly and while composing its own report. ## Where strictness fits Whichever route you take, run strict stubs. A predicate stub whose arguments never matched is invisible otherwise: the test passes because the code took a path you did not intend, and no assertion fires. Strict stubbing turns that silent mismatch into a failure and is the backstop that makes argument-dependent stubbing safe to use at all.
- Give a case where only a custom matcher will do.Argument-dependent stubbing: when(pricing.quote(argThat(r -> r.items().size() > 10))).thenReturn(bulkQuote) alongside a default stub for smaller requests. The condition has to participate in matching so the right answer is chosen at call time. An assertion after the fact cannot change what the mock returned, so no amount of post-hoc checking replaces it.
- How do you keep argument assertions from becoming brittle during refactors?Pin only the fields that carry the behaviour the test is about, and make the ignoring explicit - excluded fields in a recursive comparison, or a matcher description that names exactly what it checks. Reserve whole-object equality for small value types with real equals(). That way construction changes touch few tests, and the ones that break are the ones that should.
saying these in an interview costs you the question
- Claiming extracted assertions can always replace matchers, ignoring argument-dependent stubbing
- Putting large multi-field predicates inside verify and accepting boolean-only failures
- Asserting every field of every DTO and calling the coupling rigour
- Embedding assertion-library calls inside a matcher to get messages
- Ignoring strict stubbing when using argument-dependent stubs