skip to content

When a mocked call takes a large request object, how do you decide between matching it with `eq` on the whole object, a `match { }` predicate on a few fields, or `any()` — and what policy would you set for a team?

level: principalimportance: should knowfreq 25%

answer

  1. over-tight = churn, over-loose = rot
  2. one test owns the full shape
  3. fixture + copy kills the churn argument
  4. inject clock/ids to make eq viable
  5. any() on a meaningful arg needs justification

basics

~20 s

Match on exactly what the test claims. eq on the whole object when the object is the contract and has real value equality; a narrow predicate when only some fields are the claim; any() when the argument is irrelevant here and asserted elsewhere. Never let looseness be accidental.

solid answer

~60 s

Matcher precision is a suite-level property, not a per-line style choice. Two failure modes bound the decision: **over-tight** matchers churn — every unrelated field added to the request breaks dozens of tests; **over-loose** matchers rot — the suite stays green while the code sends the wrong thing. My decision rule: - The object *is* the contract and has genuine value equality (a data class, no arrays or timestamps) → `eq` on the whole object, ideally built from a shared fixture with `copy` overriding just the fields under test. One assertion, real diff on failure. - Only some fields are the claim → a **named** predicate on those fields, so intent is readable and unrelated fields cannot break it. - The argument is irrelevant to *this* test and pinned elsewhere → `any()`, deliberately. - Volatile fields (now(), random ids) → never `eq` the whole object; inject clock and id generator, or exclude them via a narrow matcher. Policy I would set: exactly one test owns the full-object shape; other tests match narrowly; `any()` on a semantically important argument needs a comment or a sibling test that pins it.

code

kotlin · 3 lines
kotlin
val baseRequest = ChargeRequest(customerId = 1, amount = 100, currency = "EUR", idempotencyKey = "k")

verify { gateway.charge(eq(baseRequest.copy(amount = 250))) }

go deeper

for a junior

Say that the matcher should reflect what the test is actually checking, and that any() means the argument is not being checked.

for a middle

Contrast over-tight and over-loose matching with concrete consequences and mention fixtures with copy to avoid duplication.

for a senior

Give a decision rule keyed on equality quality, volatile fields and what each test claims, plus named matcher extensions and capture-and-assert for rich checks.

for a principal

Turn it into a reviewable policy with a cost model, and connect loose matching back to weak domain types as the root cause.

## The tension Every argument matcher is a claim about what the code under test must send. Choosing precision is choosing where on a spectrum that claim sits. - **Too precise**: `eq(bigRequest)` asserts every field. Add an optional field to the request three months later and dozens of unrelated tests fail even though nothing they cover has changed. The team learns that test failures are noise, which is far more expensive than the tests are worth. - **Too loose**: `any()` everywhere asserts almost nothing. The suite is green when the code sends the wrong customer id, and the defect ships. Loose matchers are worse than missing tests because they buy false confidence. The interesting part is that neither extreme is a rule violation — both are appropriate somewhere. What matters is that the precision is *chosen*, and that the suite as a whole covers the object's shape exactly once. ## The decision rule I use **1. Does the object have real value equality?** A Kotlin data class of plain fields does. A class holding arrays, a `BigDecimal` with variable scale, a timestamp taken from the system clock, or a random id does not behave well under whole-object equality. If equality is unreliable, whole-object `eq` is out before anything else is considered. **2. Is the object the contract?** For an adapter whose job is to build a request from domain input, yes — the request's exact shape is the behaviour under test. There, `eq` on a fully-specified expected object is the *right* assertion, and the failure diff is genuinely useful. For a service that happens to pass a request through on its way to something else, no. **3. What does this specific test claim?** A test named "retries on a 5xx" claims nothing about the payload's shipping address. Matching the whole object there imports every other test's concerns into this one. A narrow, named predicate on the one or two fields that matter keeps the test honest. **4. Is the argument covered anywhere?** `any()` is fine if some sibling test pins the shape. If no test does, `any()` means the field is untested — which may be a deliberate risk decision, but should be a conscious one. ## Techniques that make precision cheap **Shared expected-object fixtures with `copy`.** Build a canonical request once; each test derives its expectation with `copy(field = value)`. Adding a field to the request means updating one fixture, not fifty tests. This single practice removes most of the churn argument against whole-object `eq`. **Named matcher extensions.** `fun MockKMatcherScope.requestFor(id: Long) = match<Request> { it.customerId == id }` turns a lambda into vocabulary. Tests read as claims, and the predicate has one definition to maintain. **Inject the volatile.** Clocks, id generators and random sources are the main reason people give up on precise matching. Injecting them makes `eq` viable and improves the production design at the same time. **Capture for rich assertions.** When the claim is really "these five fields are right", capturing the argument and asserting with a proper assertion library yields a field-level diff on failure — much better than a matcher that only reports "did not match". ## The policy For a team I would write it down as four lines: 1. **One owner per shape.** Exactly one test asserts the full object; everyone else matches narrowly on what they claim. 2. **No accidental looseness.** `any()` on a semantically meaningful argument is allowed only when another test pins it, and the reviewer should ask. 3. **No whole-object `eq` on objects containing clocks, random ids, or arrays.** Fix the source of volatility instead. 4. **Predicates get names.** Anonymous lambdas repeated across files become matcher extensions. ## How I would justify it The cost model is asymmetric. A too-tight matcher fails loudly and immediately, on the day someone changes a field; the fix is mechanical and the information is visible. A too-loose matcher fails silently, possibly in production, and no one knows the test was never checking. So when in doubt I bias toward precision *on the fields the test names*, and toward `any()` on fields it does not — rather than toward whole-object equality everywhere, which is precision without selectivity and generates exactly the churn that makes teams abandon precision altogether. The deeper signal: if matching an argument is hard, the argument type is usually the problem. Weak equality, embedded timestamps and grab-bag request objects push tests toward looseness. Improving those types improves the tests for free — which is why I treat a suite full of `any()` as a design smell rather than a testing-style preference.

  • A teammate says whole-object `eq` is always safest because it checks everything. How do you respond?
    It checks everything including things the test does not claim, so it fails for unrelated changes and trains the team to treat failures as noise. Safety comes from the suite covering the shape once and each test asserting its own claim. A shared fixture with `copy` gives you precision where it belongs without spreading it across every test.
  • The request object contains a timestamp taken from `Instant.now()`. What do you do?
    Inject a clock so the test can fix the instant, which makes whole-object matching viable and also makes the production code testable in other ways. If injecting is not possible immediately, match on the stable fields with a named predicate rather than loosening the whole argument to `any()`, so the rest of the claim is still checked.

saying these in an interview costs you the question

  • Treating `any()` as a safe default because it avoids failures.
  • Using whole-object equality on requests containing timestamps or random ids.
  • Duplicating fully-specified expected objects across dozens of tests.
  • Arguing precision purely as style, with no account of churn versus false confidence.
  • Never asking whether the argument type itself is the reason matching is hard.

context