skip to content

Suspend Function Mocking

coEvery and coVerify are the suspend-aware twins of every and verify. Interviewers check you know when the co- variants are required and what happens if you use the wrong one.

on this pageshow

explore

questions

4

A collaborator exposes a suspend function that returns Unit. How do you stub it in MockK without inventing a return value, and what exactly does MockK's coJustRun do?

level: middleimportance: should knowfreq 45%

answer

  1. strict mock: no answer found for
  2. coJustRun = coEvery + just Runs
  3. suspend block ⇒ co prefix
  4. relaxUnitFun stubs all Unit members
  5. stubbing ≠ verifying: still coVerify

basics

~20 s

Write coJustRun { audit.record(any()) }. It is MockK's suspend-capable shorthand for coEvery { audit.record(any()) } just Runs: the call is recorded, does nothing and returns Unit. Creating the mock with relaxUnitFun = true stubs every Unit function instead.

solid answer

~50 s

On a strict MockK mock every call needs an answer, including suspend functions that return `Unit` — otherwise the call throws a `MockKException` starting with *no answer found for*. `coJustRun { audit.record(any()) }` is the suspend-capable form of `justRun`; it expands to `coEvery { audit.record(any()) } just Runs`, i.e. record the call, do nothing, return `Unit`. Use it when you only care that the call happened. The alternatives are: stub it explicitly with `coEvery { … } just Runs` when you also want to chain later answers; or push the decision to creation time with `mockk<Audit>(relaxUnitFun = true)` / `@MockK(relaxUnitFun = true)`, which pre-answers **all** Unit-returning members, suspend ones included, so no per-function stub is needed. Stubbing is independent of verification: the call is still verified with `coVerify { audit.record("checkout") }`, never with `verify`, because a suspend call cannot appear inside a non-suspend verification block.

code

kotlin · 12 lines
kotlin
interface Audit { suspend fun record(event: String) }

val audit = mockk<Audit>()
coJustRun { audit.record(any()) }
// identical to:
// coEvery { audit.record(any()) } just Runs

runBlocking { audit.record("checkout") }
coVerify { audit.record("checkout") }

// creation-time alternative, covers every Unit member at once
val relaxedAudit = mockk<Audit>(relaxUnitFun = true)

go deeper

for a junior

Know that a strict mock needs an answer for every call and that coJustRun is the do-nothing answer for suspend functions returning Unit.

for a middle

State the expansion (coEvery … just Runs), why the co prefix exists at all, and that relaxUnitFun is the creation-time alternative covering all Unit members.

for a senior

Argue the tradeoff: per-call coJustRun keeps unexpected interactions failing loudly, relaxUnitFun trades that safety for brevity on chatty sinks; note that stubbing and verification stay separate concerns.

for a principal

Frame it as a team default — which collaborators get relaxUnitFun at creation, and where strictness is worth the extra lines because an unexpected side-effect call should break the build.

## Why a Unit-returning suspend function still needs a stub A MockK mock created with `mockk<T>()` is *strict*: it has no behaviour at all until you record one. When production code calls a member the mock has no answer for, MockK throws a `MockKException` whose message begins with `no answer found for:` followed by the mock name and the call. The return type is irrelevant — a function declared `suspend fun record(event: String)` returns `Unit`, but MockK will not silently invent `Unit` for you on a strict mock. `Unit` is a value like any other, and the strictness is deliberate: it forces every interaction the test relies on to be visible in the test. ## coJustRun: the suspend form of the do-nothing stub MockK ships a pair of shorthands for that case. `justRun { obj.method() }` is the non-suspend one; `coJustRun { obj.suspendMethod() }` is the suspend-capable one — its block is a suspending lambda, so a suspend call may legally appear inside it. Semantically `coJustRun { audit.record(any()) }` is exactly `coEvery { audit.record(any()) } just Runs`, which itself is `coEvery { … } returns Unit`. Three spellings, one behaviour: MockK records a stub for the matched call, the call does nothing when invoked, and the invocation is added to the mock's recorded-call list so verification can see it. The reason a separate `co*` function exists at all is Kotlin's type system, not MockK's internals. The DSL block of `justRun`/`every`/`verify` is an ordinary lambda; you cannot call a suspend function inside one, so the code would not compile (`Suspension functions can be called only within coroutine body`). The `co*` variants declare their block as `suspend`, so the recording call compiles. ## Creation-time alternative: relaxUnitFun If a collaborator has many fire-and-forget members — loggers, audit sinks, metric recorders, event publishers — stubbing each one is noise. Create the mock with `mockk<Audit>(relaxUnitFun = true)` or annotate it `@MockK(relaxUnitFun = true)`, and MockK pre-answers every member whose return type is `Unit`, including suspend members, while leaving value-returning members strict. `MockKAnnotations.init(this, relaxUnitFun = true)` applies the same flag to all annotated mocks in the class at once. The tradeoff is the usual one: per-call `coJustRun` keeps the test's assumptions explicit and still fails loudly if the code calls a *different* Unit member you did not expect; `relaxUnitFun` is quieter and shorter. ## Stubbing is not verification A very common confusion is treating `coJustRun` as an assertion. It is not — it only supplies behaviour. If the test's point is "the audit entry is written", you still need `coVerify { audit.record("checkout") }`. And it must be `coVerify`, not `verify`: the verification block for a suspend call has to be a suspending lambda for the same compile-time reason as above. ## When coJustRun is the wrong tool - **You need more than one answer.** `coJustRun` states a single do-nothing behaviour. When the first call must succeed and a later one must fail, write the stub out with `coEvery` and chain answers explicitly. - **You need the argument.** If the test wants to inspect what was passed, either capture it, or use `coAnswers { … }` so the answer body can read the arguments. - **The function returns a value.** `coJustRun` only makes sense for `Unit`; anything else needs a real answer. ## Mental model `coJustRun` sits at the intersection of two axes MockK keeps orthogonal: *suspend vs blocking* (which decides whether you use the `co` prefix) and *what answer to give* (which decides the terminator). `coJustRun` is simply the point "suspend" × "do nothing, return Unit", packaged as one call because that point is hit constantly in coroutine-heavy code.

  • What happens if the production code calls a value-returning suspend function on a mock created with relaxUnitFun = true?
    Nothing changes for it — `relaxUnitFun` only pre-answers members whose return type is `Unit`. A value-returning suspend function is still strict, so the call throws a `MockKException` starting with "no answer found for". That is usually what you want: the fire-and-forget calls stay quiet while the calls whose results actually drive the code under test must be stubbed deliberately.
  • Why is there a coJustRun at all instead of just using justRun everywhere?
    Because the DSL block of `justRun` is an ordinary, non-suspending lambda, so writing a suspend call inside it does not compile — Kotlin rejects it with "Suspension functions can be called only within coroutine body". `coJustRun` declares its block as `suspend`, so the recording expression compiles. The recorded stub itself is the same in both cases.

saying these in an interview costs you the question

  • Assuming a Unit-returning function needs no stub on a strict mock — MockK still throws "no answer found for".
  • Thinking coJustRun asserts the call happened; it only supplies behaviour, you still need coVerify.
  • Believing coJustRun makes the whole mock relaxed — it stubs exactly the matched call.
  • Trying to verify the suspend call with verify { } and blaming MockK when it does not compile.

context

open as a page

A single MockK mock stands in for an interface that has both suspend and ordinary functions. How do you stub and verify each kind, and can one verification block cover calls of both kinds?

level: middleimportance: should knowfreq 48%

basics

~20 s

One mock handles both: use every/verify for ordinary members and coEvery/coVerify for suspend ones. The co-variants also accept ordinary calls, so coVerifySequence can cover a mixed run; the plain verify block cannot contain a suspend call at all.

open as a page

MockK lets you finish a stub for a suspend function with either `answers { }` or `coAnswers { }`. What is the concrete difference between the two blocks, and in whose coroutine does a `coAnswers` body execute?

level: seniorimportance: should knowfreq 42%

basics

~20 s

coAnswers takes a suspending lambda, so it may call delay or other suspend functions; the plain answers block is non-suspending and cannot. The coAnswers body runs inline in the caller's coroutine, so the caller's dispatcher and cancellation apply to it.

open as a page

You need a test to prove that your service abandons a slow dependency under withTimeout. How do you make a MockK-stubbed suspend function actually suspend, and what must you avoid?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Stub it with a suspending answer: coEvery { client.fetch() } coAnswers { delay(10_000); Response }. The delay runs in the caller's coroutine, so it is a real cancellable suspension point. Never fake latency with Thread.sleep in a plain answers block.

open as a page