A collaborator you are mocking takes a lambda parameter — something like retry(times: Int, block: () -> String). Using MockK, how do you get hold of that lambda and actually run it, and what is the difference between doing it inside an answers block and capturing it into a slot for later?
answer
- captureLambda() matches, lambda<T>() retrieves in answers
- invoke now → mock behaves like the real wrapper
- slot<(T) -> Unit>() → fire later from the test
- captureCoroutine() + coroutine<T>().coInvoke() for suspend
- stubbing any() drops the block silently
basics
~20 sUse MockK's captureLambda() in the every block and invoke it from the answers block via lambda<() -> String>().invoke(). To fire it later instead, capture it into a normal slot — slot<() -> String>() with capture(slot) — and call slot.captured() from the test after the call returns.
solid answer
~50 sTwo shapes, chosen by *when* the lambda must run. **During the call** — `every { retrier.retry(any(), captureLambda()) } answers { lambda<() -> String>().invoke() }`. `captureLambda()` is a matcher that accepts the function argument and stores it in the answer scope's lambda slot; inside `answers { }` you retrieve it with `lambda<T>()` and invoke it. This makes the mock behave like the real component: the block runs synchronously, its exceptions propagate to the caller, and its result can become the stub's return value. **After the call** — a lambda is just a value, so `val cb = slot<(Result) -> Unit>()` with `capture(cb)` works like any other capture. The test then calls `cb.captured(Result.Ok("data"))` at a moment of its choosing. That is the shape for registration/callback APIs where production hands over a listener and something fires it later. MockK offers the suspend equivalents `captureCoroutine()` and `coroutine<T>().coInvoke(...)` when the parameter is a suspending function.
code
kotlin · 6 linesinterface Retrier { fun retry(times: Int, block: () -> String): String }
val retrier = mockk<Retrier>()
every { retrier.retry(any(), captureLambda()) } answers {
lambda<() -> String>().invoke()
}go deeper
Know that a lambda argument can be captured and invoked, and be able to show the captureLambda() + answers { lambda<...>().invoke() } shape.
Explain both placements and why a stub that ignores the block makes the test cover nothing; name the type-argument pitfall.
Choose by contract — invoke-now for wrappers, capture-for-later for callback registration — and drive multi-phase async tests deterministically from captured callbacks.
Discuss when mocking a block-taking collaborator is worth it at all versus using the real wrapper, and keep a suite convention so lambda-dropping stubs cannot silently erase coverage.
## Why lambdas need their own story A function argument in Kotlin is an ordinary value, so a lambda parameter can be matched with `any()` and captured with `capture(slot)` like anything else. What makes it interesting is that a test usually does not want to *inspect* the lambda — you cannot meaningfully assert on a function object — it wants to *run* it, because the interesting logic lives inside the block the production code passed in. If the mock swallows the block, that code is never executed and the test silently covers nothing. ## Running it during the call: captureLambda + answers `captureLambda()` is a MockK matcher written in the argument position of the function parameter. It matches the function argument and stores it where the answer scope can find it. Inside the `answers { }` block, `lambda<T>()` gives you that captured function typed as `T`, and MockK provides `invoke` support so you can call it directly: ```kotlin every { retrier.retry(any(), captureLambda()) } answers { lambda<() -> String>().invoke() } ``` The type argument to `lambda<...>()` must match the real parameter type — MockK cannot infer it from the answer block, and getting it wrong surfaces as a cast failure at run time rather than a compile error. Note what this stub *is*: a mock that behaves like the real wrapper. The block runs synchronously inside the call, whatever it returns becomes the value the answer produces, and an exception thrown by the block propagates out of the mocked call exactly as it would in production. That is precisely what you want when mocking transaction wrappers, retry helpers, `use`-style resource scopes or `measureTime`-shaped utilities: the code under test only makes progress if the block actually runs. ## Running it later: capture into a slot When the collaborator's contract is "take this callback and call it eventually", the test wants control over *when*. Because a lambda is a value, a plain slot works: ```kotlin val cb = slot<(Result) -> Unit>() every { client.fetch(any(), capture(cb)) } just Runs service.load() cb.captured(Result.Ok("data")) ``` The stub returns immediately having done nothing, and the test then fires the callback to drive the next phase — success first, then re-run and fire a failure. This gives deterministic tests for asynchronous-looking APIs without any real threading. The same slot rules apply as for values: the slot holds the *last* captured lambda, and `isCaptured` tells you whether production registered one at all — a useful assertion in its own right ("the service registered a listener"). ## Suspend lambdas If the parameter is a suspending function, the ordinary lambda helpers cannot invoke it. MockK provides the coroutine-aware pair: `captureCoroutine()` as the matcher and `coroutine<T>().coInvoke(...)` inside a `coAnswers { }` block to call it from a suspending context. The shape is otherwise identical. ## Choosing between the two Ask what the collaborator's contract is: - *It runs my block and returns its result* (transaction, retry, lock, timing wrapper) → `captureLambda()` + `answers`, invoking immediately. Anything else means the block is never exercised and coverage lies. - *It stores my block and calls it later* (listener registration, async callback, event subscription) → capture into a slot and invoke from the test when you want that phase to happen. ## Failure modes to expect - **The block never runs.** A stub written as `every { tx.inTransaction(any()) } returns Unit` matches happily and drops the lambda; every assertion about work done inside the transaction fails, or worse, passes vacuously. Reach for `captureLambda` the moment the parameter is a function you rely on. - **Wrong type parameter.** `lambda<() -> Unit>()` where the real type is `(Int) -> Unit` fails at run time, not compile time. - **Invoking a slot that was never captured.** If no call matched, `isCaptured` is false and calling `captured` is meaningless — assert the registration happened first. - **Re-registration.** If production registers several callbacks and you hold one slot, you invoke only the last; capture into a `MutableList` when there can be more than one.
- What actually goes wrong if you stub a transaction wrapper as `every { tx.inTransaction(any()) } returns result` instead of invoking the block?The mock matches and returns the canned value without ever running the lambda, so every side effect the test believes happened inside the transaction never occurs. The test either fails on an unrelated assertion or, worse, passes while covering none of the block's logic. Capturing the lambda and invoking it keeps the mock faithful to the wrapper's contract.
- The parameter is a suspending block. What changes?The ordinary lambda helpers cannot call a suspending function, so MockK provides the coroutine variants: `captureCoroutine()` as the matcher and `coroutine<T>().coInvoke(...)` inside a `coAnswers { }` block, which supplies the suspending context. The decision between invoking immediately and capturing for later is unchanged.
saying these in an interview costs you the question
- Stubbing a lambda parameter with any() and assuming the block still runs
- Thinking captureLambda() can be read outside the answers block like a normal slot's captured value
- Believing MockK infers the lambda's type so the type argument to lambda<T>() does not matter
- Trying to assert equality on a captured lambda object instead of invoking it
- Holding one slot while production registers several callbacks and expecting to reach the first one