You already have a Kotest Matcher<String> and want to apply the same rule to a field of a domain object, and separately to a nullable value. What does Matcher's contramap give you, and what problem does Kotest's neverNullMatcher helper solve?
answer
- Matcher<in T> is contravariant → adapt the input
- contramap(f: (U) -> T): Matcher<U>
- inherited message describes the projected value
- neverNullMatcher: Matcher<T?> from a non-null test
- null = clean failure, not NPE
basics
~20 scontramap adapts a matcher's input type: Matcher<String>.contramap { o: Order -> o.ref } yields a Matcher<Order> that applies the same rule to the projected field. neverNullMatcher wraps a non-null test into a Matcher<T?> that fails with a message on null instead of throwing NPE.
solid answer
~50 s`Matcher<in T>` is contravariant in its input, so adapting a matcher means adapting the value going *in*. `contramap(f: (U) -> T): Matcher<U>` does exactly that: give it a projection from your type to the matcher's type and you get a matcher over your type, reusing the original rule and messages. `beUpperCase().contramap { order: Order -> order.ref }` is a `Matcher<Order>` with no rule duplicated. (Kotest material has also referred to this input-adapting operation as `compose`.) The catch is diagnostics: the inherited message describes the *projected* value (`"abc" should be upper case`) with no mention of which order it came from. When that matters, wrap the result and re-word, or include the owner in the projection's rendering. `neverNullMatcher { value -> … }` builds a `Matcher<T?>` from a test written against a non-null `T`. If the value is null the matcher fails with a proper message rather than letting the body NPE, so a null in production data reads as an assertion failure, not a crash.
code
kotlin · 15 linesdata class Order(val id: Long, val ref: String)
fun beUpperCase(): Matcher<String> = Matcher { s ->
MatcherResult(s == s.uppercase(), { "\"$s\" should be upper case" }, { "\"$s\" should not be upper case" })
}
// same rule, applied to a field - no predicate duplicated
val haveUpperCaseRef: Matcher<Order> = beUpperCase().contramap { order: Order -> order.ref }
Order(1, "AB-1") should haveUpperCaseRef
// null fails with a message instead of throwing NPE inside the matcher
fun beNonBlank(): Matcher<String?> = neverNullMatcher { value ->
MatcherResult(value.isNotBlank(), { "expected a non-blank string" }, { "expected a blank string" })
}go deeper
Know that contramap adapts the input type and that neverNullMatcher stops nulls from throwing inside a matcher.
Show a concrete projection and explain why the direction is (U) -> T rather than the other way round.
Discuss the message-context loss after adaptation, how to re-word without duplicating the rule, and why an NPE inside test() is a worse failure than a reported one.
Frame it as library design: one core predicate per rule, adapters at the edges, an explicit team policy on whether null violates or vacuously satisfies a rule.
## Why input adaptation, not output adaptation `Matcher<in T>` is **contravariant** in `T`: it consumes values and produces a `MatcherResult`. Contravariance is why a `Matcher<Any>` is usable everywhere a `Matcher<String>` is expected — a rule that accepts anything certainly accepts strings. It also dictates the shape of the adapter: to move a matcher from type `T` to type `U`, you supply a function `(U) -> T` that turns *your* value into the value the matcher already understands. That is `contramap`, the mirror image of the familiar `map`. ``` Matcher<String>.contramap { u: Order -> u.ref } ==> Matcher<Order> ``` The returned matcher, on `test(order)`, applies the projection and delegates. Nothing about the rule or its messages is rewritten — which is both the benefit and the limitation. (Kotest documentation and older APIs have described this same input-adapting operation under the name `compose`; on Kotest 5.x `Matcher` it is `contramap`. Whichever name you meet, the direction is the same: adapt the input, not the result.) ## What contramap buys you - **One rule, many carriers.** A validated-reference rule written once as `Matcher<String>` is reusable against `Order.ref`, `Invoice.reference`, a map entry, or a parsed token — without a single duplicated predicate. - **Composability survives.** The adapted matcher is an ordinary matcher, so it still works with `and`, `or`, `invert()`, inspectors, and any API taking a `Matcher<U>`. - **Test-only projections.** The projection is just a lambda, so you can adapt through computed properties, not only fields. ## The diagnostic cost The inherited failure message talks about the projected value, because that is the only value the underlying matcher ever saw. `"ab-1" should be upper case` gives no clue that it came from order 4711's reference field. Options, in order of preference: 1. Wrap the adapted matcher in a thin matcher that re-words the message using the *outer* value. 2. Project to a rendering that carries context (`"order ${o.id} ref=${o.ref}"`) when the underlying rule is string-shaped anyway. 3. Accept it — fine for a locally scoped assertion, poor for a matcher a whole team reads CI output from. This is the same trade-off as `and`/`or`: reuse gives you a correct predicate cheaply and a mediocre message with it. ## Nullability: the NPE trap A matcher written as `Matcher<String>` cannot be applied to a `String?` without a cast or an assertion first. Developers reach for `Matcher<String?>` and then write the body as if the value were present — the first null then throws a `NullPointerException` from inside `test()`. That is a bad failure: it escapes the matcher's reporting contract, produces a stack trace instead of a message, and inside a soft-assertion block it is not a collected assertion failure at all. `neverNullMatcher<T : Any>(test: (T) -> MatcherResult): Matcher<T?>` is the fix. You write the body against a **non-null** `T`; the helper returns a `Matcher<T?>` that checks for null first and reports a clean failure when the value is null, only invoking your body otherwise. Practically: - Your rule stays free of `?.`/`!!` noise. - Null becomes a first-class assertion failure with a message, participating in clue context and failure collection like any other. - The matcher is still usable against non-null values, since `T` is a subtype of `T?`. A design decision comes with it: is null a **violation** of your rule or **vacuously acceptable**? `neverNullMatcher` encodes the first answer, which is almost always the one you want in tests — a missing value should not silently satisfy an assertion about its content. If you genuinely want null to pass, write that branch explicitly so the intent is visible. ## Putting them together The two helpers compose naturally: write the core rule against the non-null primitive type, wrap it with `neverNullMatcher` if the carrier may be absent, then `contramap` it onto each domain type that holds such a value. The core predicate is written and tested once; everything else is plumbing. ## Common mistakes - Reaching for `map` semantics and trying to adapt the *result*; the type system pushes back because matchers consume, not produce. - Duplicating the rule per domain type because the projection was not obvious. - Doing work in the projection lambda that can itself throw (parsing, `!!`), which reintroduces the exact failure mode `neverNullMatcher` removes — keep projections total. - Shipping an adapted matcher with an inherited message so generic that CI output cannot identify the offending object.
- After contramap, the failure message no longer identifies which object failed. How do you fix that without duplicating the rule?Wrap the adapted matcher in a thin outer matcher that calls it and rebuilds the message using the outer value — `MatcherResult(inner.passed(), { "order ${o.id}: ${inner.failureMessage()}" }, …)`. You keep one source of truth for the predicate while owning the wording at the level where the reader needs context.
- Why is Matcher declared as Matcher<in T> rather than Matcher<T>?Because a matcher only consumes values. Declaring the parameter `in` makes the type contravariant, so a `Matcher<Any>` can legally be used wherever a `Matcher<String>` is required — a rule that accepts any value certainly accepts strings. That same consuming position is why adaptation is `contramap`, taking `(U) -> T`, rather than a `map` over the output.
saying these in an interview costs you the question
- Expecting to adapt a matcher by mapping its result instead of its input
- Writing Matcher<String?> with a body that dereferences the value and NPEs on null
- Assuming an NPE from inside a matcher is reported like a normal assertion failure
- Duplicating a rule for every domain type that carries the value
- Shipping an adapted matcher whose inherited message cannot identify the failing object