skip to content

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