MockK's `match { }` lets you supply an arbitrary predicate as an argument matcher. What are the mechanics and the downsides, and how do you keep predicate-based matching debuggable?
answer
- predicate runs per candidate call, in the matching loop
- pure, cheap, must not throw
- opaque failure message — no description
- name it: extension on MockKMatcherScope
- matchNullable / coMatch variants
basics
~20 smatch { } stores your lambda as the matcher and runs it against each candidate argument at match time. The downside is diagnostics: a failing lambda prints as an opaque matcher. Keep predicates small, pure, and named via reusable extensions, or use specific matchers instead.
solid answer
~60 s`match { it.total > 0 }` records a predicate as the matcher for that argument position. At call time — and again during verification, against each recorded call — MockK runs the lambda and takes the boolean. Three mechanical consequences: - The lambda is executed repeatedly, so it must be **pure and cheap**; side effects or heavy work distort the test. - It should not throw. An exception inside a predicate surfaces from matching, not as a clean assertion failure, and is confusing to diagnose. - Failure output can only describe *a* predicate, not what it wanted. Unlike `eq`, `range` or `ofType`, a lambda has no readable description, so a mismatch report shows the actual argument and an opaque matcher. My rules: prefer a specific matcher when one exists; extract recurring predicates into named extensions on MockK's matcher scope so the intent has a name; keep the predicate to the one or two fields the test actually claims something about; and when the assertion is complex, capture the argument and assert on it afterwards, where the failure message is a proper diff. `matchNullable { }` is the nullable-parameter variant, and `coMatch { }` handles suspending predicates.
code
kotlin · 5 linesfun MockKMatcherScope.orderFor(customerId: Long) =
match<Order> { it.customerId == customerId }
every { repo.save(orderFor(42)) } returns Unit
verify { audit.log(orderFor(42)) }go deeper
Know that match { } takes a boolean lambda over the argument and is the fallback when no specific matcher fits.
Explain that the predicate is evaluated during matching, keep it pure, and prefer built-in matchers where they exist.
Reason about failure diagnostics, name predicates via matcher-scope extensions, compose with and/or/not, and move real assertions to capture-and-assert.
Set the suite-level norm: predicate matchers are dispatch logic, not assertions, because failure-message quality is a property you manage across hundreds of tests.
## Mechanics Inside a recording block, `match<T> { predicate }` stores a function-backed matcher in that argument slot. Matching then works like this: - **During stubbing**, when the production code calls the mocked method, MockK evaluates the stored matchers against the incoming arguments to select an answer. Your lambda runs there. - **During verification**, MockK evaluates the matchers against *every recorded call* to that method to count matches. Your lambda runs once per candidate call. So the predicate is not evaluated once, and it is not evaluated at the point where you wrote it. It is a callback into your test's logic executed inside MockK's matching loop. That leads to a set of practical requirements: 1. **Pure.** A predicate that mutates test state, increments a counter or logs will run an unpredictable number of times. 2. **Cheap.** It may run for every recorded call on that method. 3. **Total.** It should return false rather than throw on unexpected shapes. Throwing from a matcher aborts matching in a way that reads like a MockK internal failure rather than a test assertion. 4. **Null-aware where needed.** `match { }` provides a non-null value to the lambda; nullable parameters use `matchNullable { }`, whose lambda receives the nullable value. Suspending predicates use `coMatch { }`. ## The diagnostics problem Built-in matchers describe themselves. When `eq(7)` fails, the report shows what was expected and what arrived, and a reader instantly sees the difference. When `range(1, 10)` fails, the bound is visible. A lambda has no such description — MockK can print that a matcher rejected the argument, but not the condition inside your closure. So a failing predicate-matched stub produces the least actionable failure in the whole catalog: > no answer found for: Service(#1).send(Order(id=42, total=0)) and you are left reading the test source to work out which of the three clauses inside the lambda failed. This is the core argument against making `match { }` a habit. It is the universal fallback, and universal fallbacks erode the quality of failure output across a suite. ## Techniques that keep it debuggable **Prefer a specific matcher.** `more(0)`, `range(1, 10)`, `ofType<Retryable>()`, `isNull(inverse = true)` each say something a reader and a failure message can use. A lambda that merely re-implements one of these is a strict downgrade. **Name the predicate.** Extract it as an extension on MockK's matcher scope so call sites read like a domain assertion: ```kotlin fun MockKMatcherScope.orderFor(customerId: Long) = match<Order> { it.customerId == customerId } every { repo.save(orderFor(42)) } returns Unit ``` The failure message still shows an opaque matcher, but the *test source* now names the intent, which is where the reader ends up anyway. **Keep it to what the test claims.** A predicate checking six fields will fail for a change in any of them, including fields irrelevant to this test. One or two fields keeps it both readable and robust. **Compose instead of conjoining in a lambda.** `and(more(0), less(100))`, `or(isNull(), ofType<Draft>())` and `not(m)` build compound matchers out of self-describing parts, so the description survives. **Capture when the assertion is real.** If what you want is a rich assertion on the argument — several fields, a nested structure, an assertion library's diff — capturing the argument and asserting after the fact gives a proper failure message. Matching answers "which call", asserting answers "was it right"; a predicate that is really an assertion in disguise belongs on the assertion side. ## When `match { }` is genuinely the right choice - Selecting which stub applies among several calls that differ by a computed condition — the matcher *is* dispatch logic, not an assertion. - Types with no usable `equals` where only one field identifies the call. - Cross-field conditions no built-in matcher expresses ("start before end", "currency matches the account"). In all of those the predicate is doing something the catalog cannot, and the readability cost is paid for. ## A checklist for review - Could a built-in matcher express this? Then use it. - Does the lambda mutate anything or do IO? Then fix it. - Does it check more fields than the test claims? Then narrow it. - Is it really an assertion? Then capture and assert instead. - Does it appear more than twice? Then give it a name.
- Why is throwing an assertion from inside `match { }` a bad idea?The lambda runs inside MockK's matching loop, potentially against several recorded calls, so an exception aborts matching rather than reporting a clean expectation failure. The resulting stack trace points into MockK internals and gives no diff between expected and actual. If you want assertion semantics, capture the argument and assert on it after the call.
- When would you capture the argument instead of matching it with a predicate?Whenever the check is really an assertion about correctness rather than a rule for selecting which call you mean. Capturing gives you the object in the test body, where an assertion library produces a proper field-level diff on failure. A predicate is right when it disambiguates among calls, not when it is the test's actual expectation.
saying these in an interview costs you the question
- Using `match { }` where `eq`, `range` or `ofType` says the same thing.
- Putting assertions or side effects inside the predicate.
- Assuming the predicate runs exactly once.
- Expecting MockK to print the contents of the lambda in a failure message.
- Not knowing `matchNullable`/`coMatch` exist for nullable and suspending cases.