In MockK, how do you express "this argument must be null", "must not be null", and "must be this concrete subtype" — and what are the limits of type-based matching?
answer
- any() is constant-true → matches null
- isNull() / isNull(inverse = true)
- matchNullable for nullable params
- ofType<T>() = runtime class check
- generics erased: List<String> → just List
basics
~20 sUse isNull() for null and isNull(inverse = true) for not-null; any() will not do, since it matches null too. Use ofType<T>() for a concrete subtype — it checks the runtime class, so generic arguments are erased.
solid answer
~50 sMockK's `any()` is a constant-true matcher, so on a nullable parameter it matches `null` as well as any value. That makes it the wrong tool for nullability assertions. The precise forms are `isNull()` for "must be null" and `isNull(inverse = true)` for "must be present". For type constraints, `ofType<T>()` (or `ofType(T::class)`) matches when the argument is an instance of that type. It is the natural matcher for a method that takes a sealed supertype: you can register different answers per branch, `every { handler.on(ofType<PaymentFailed>()) } returns Retry` alongside a stub for another subtype. The limit is erasure. `ofType` inspects the runtime class, so `ofType<List<String>>()` asserts only "is a List" — a `List<Int>` matches it. When the element type matters, add a predicate: `match<List<*>> { it.all { e -> e is String } }`. One more nullability detail: `match { }` gives the lambda a non-null value, so for a nullable parameter you use `matchNullable { }`.
code
kotlin · 2 linesevery { policy.decide(ofType<PaymentFailed>()) } returns Retry
every { policy.decide(ofType<PaymentSettled>()) } returns Donego deeper
Know that isNull() exists and that any() is not a not-null check.
Show isNull(inverse = true) and ofType<T>() and explain why they beat any() plus a comment.
Add the erasure limit, the matchNullable distinction, and dispatching multiple stubs on a sealed hierarchy by type.
Point at the design lever: when matchers have to compensate for erasure or nullable-everything APIs, the domain types are the real problem.
## Nullability: why `any()` is the wrong tool `any()` is implemented as a matcher that returns true for every candidate argument. There is no null filter in it. On a parameter declared `String?`, `every { svc.send(any()) }` will therefore be satisfied by a `null` argument just as happily as by `"abc"`. Tests written on the assumption that `any()` means "any real value" quietly stop catching the bug where a null slips through. The explicit forms: ```kotlin every { svc.send(isNull()) } throws IllegalArgumentException("no payload") every { svc.send(isNull(inverse = true)) } returns Ack ``` `isNull()` matches only null; the inverse flag flips it to "only non-null". These read as assertions, which is the point — the matcher documents the branch under test. They also let you register two different answers on the same method keyed on nullability, which is a compact way to test both paths without two mocks. A closely related detail: MockK's predicate matcher `match { }` is typed so the lambda receives a non-null value. For a nullable parameter, that shape does not fit, and MockK provides `matchNullable { }`, whose lambda receives the nullable value and can inspect null itself. The same family logic applies to capture: there is a nullable variant for the same reason. ## Type matching with `ofType` `ofType<T>()` matches when the incoming argument is an instance of `T`. There is also a form taking a `KClass`. It is most valuable where the declared parameter type is broader than what a given test cares about: ```kotlin sealed interface DomainEvent data class PaymentFailed(val id: String) : DomainEvent data class PaymentSettled(val id: String) : DomainEvent every { policy.decide(ofType<PaymentFailed>()) } returns Retry every { policy.decide(ofType<PaymentSettled>()) } returns Done ``` Two stubs on one method, dispatched by type. Without `ofType` you would need value-level equality on events you may not want to construct exactly, or a predicate doing `it is PaymentFailed`, which is the same thing spelled less clearly. `ofType` is also useful for overload-ish situations where a method takes `Any` — logging, event buses, serialization boundaries — and for asserting that the code passed the *right kind* of thing without pinning its contents. ## The erasure limit JVM generics are erased, so a runtime class check cannot see type arguments. `ofType<List<String>>()` compiles, but at match time all it can ask is "is this object a `List`?" — a `List<Int>` passes. The same holds for `Map<K, V>`, `Result<T>` and any other generic container. When the type argument is the point, combine matchers: ```kotlin every { sink.accept(match<List<*>> { it.isNotEmpty() && it.all { e -> e is String } }) } just Runs ``` or, better, restructure so the domain has a real type — a `Recipients` value class rather than a `List<String>` — which makes both the production code and the matcher unambiguous. A second, subtler limit: `ofType` sees the *runtime* class, which for a mocked argument is MockK's generated class implementing the mocked type. In practice this behaves as expected (the mock is an instance of the mocked type), but a check for a specific concrete implementation class will not be satisfied by a mock of its interface. ## Composing nullability and type The combinators let you express compound constraints without a lambda: ```kotlin every { audit.record(and(isNull(inverse = true), ofType<SecurityEvent>())) } just Runs every { cache.put(not(isNull()), any()) } just Runs ``` `and`, `or` and `not` take matchers and produce a matcher, so any two members of the catalog compose. ## Choosing in practice - Asserting a null-handling branch → `isNull()` / `isNull(inverse = true)`, never `any()`. - Dispatching on a sealed hierarchy → `ofType<Branch>()`, one stub per branch. - Generic container where the element type matters → `ofType` is not enough; add a predicate or improve the domain type. - Nullable parameter needing a predicate → `matchNullable { }`, because `match { }` hands you a non-null value. The recurring theme is that the matcher should state the *claim*. `any()` on a nullable parameter states almost nothing; `isNull(inverse = true)` states something a reviewer can check.
- A test uses `verify { svc.send(any()) }` to prove a non-null payload was sent. What is wrong with it?`any()` is a constant-true matcher, so the verification also passes when the code sent `null`. The test therefore does not prove what its author intended. `isNull(inverse = true)` states the actual claim, and the verification then fails on the null-payload bug.
- How would you match a `List<String>` argument specifically, given erasure?`ofType<List<String>>()` can only check that the argument is a `List`, so add an element-level predicate such as `match<List<*>> { it.all { e -> e is String } }`, accepting that an empty list is ambiguous. The more durable fix is a domain type — a value class wrapping the list — so that the type itself carries the meaning at runtime.
saying these in an interview costs you the question
- Using `any()` as a not-null assertion.
- Believing `ofType<List<String>>()` checks the element type.
- Not knowing `matchNullable` exists and forcing a nullable parameter through `match`.
- Assuming `isNull` has no inverse and writing a predicate instead.
- Thinking type-based stubs cannot coexist on the same method.