skip to content

You mocked a collaborator with MockK and the code under test calls one of its Unit-returning methods. Why does that call still fail on a non-relaxed mock, and what exactly does `just Runs` do about it?

level: middleimportance: should knowfreq 45%

answer

  1. strict mock: no answer for ANY call, Unit included
  2. Runs = marker object; just Runs = constant Unit answer
  3. justRun { } = same thing, one call
  4. stubbed ≠ invisible — verify still sees it
  5. Unit functions can throws / answers / chain too

basics

~20 s

A plain mock has no answer for any call, including Unit ones, so it throws a MockKException. every { x.log(any()) } just Runs registers the answer "return Unit and do nothing". justRun { x.log(any()) } is the same thing in one call.

solid answer

~50 s

On a non-relaxed `mockk()`, *every* member is unstubbed, and a Unit return type is no exception — MockK still has to be told what the call does, so an unstubbed invocation fails with a MockKException saying no answer was found. Writing `returns Unit` would work but reads badly, so MockK offers a dedicated terminator: ```kotlin every { auditLog.record(any()) } just Runs justRun { auditLog.record(any()) } // equivalent shorthand ``` `Runs` is a MockK marker object; `just Runs` simply registers a constant `Unit` answer. It is not "call the real method" and it is not "ignore the call" — the invocation is still **recorded**, so `verify { auditLog.record(any()) }` works afterwards. Because it is an ordinary terminator, the Unit function can be stubbed any other way too: `throws` to simulate a failing sink, or an `answers { }` block if the stub needs a side effect. And `just Runs` composes into a sequence like any other first answer.

code

kotlin · 7 lines
kotlin
val audit = mockk<AuditLog>()
every { audit.record(any()) } just Runs
// or: justRun { audit.record(any()) }

service.transfer(100)

verify(exactly = 1) { audit.record(any()) }  // still visible

go deeper

for a junior

Say that a plain mock answers nothing at all, so even a Unit method must be stubbed, and that just Runs (or justRun) is the idiomatic way to say "do nothing".

for a middle

Explain that just Runs registers a constant Unit answer, that the real method is not called, and that the invocation is still recorded for verification.

for a senior

Bring up the tradeoff against relaxing the whole mock, and show you use Unit stubs actively — throwing from a Unit sink to test degradation paths, or chaining just Runs into a later failure.

for a principal

Take a position on strictness as a team default: where blanket relaxation is acceptable, where targeted just Runs is required, and how that choice affects how quickly unplanned collaborator calls surface.

## Why a Unit function needs stubbing at all A MockK mock created with `mockk<T>()` is *strict*: it has no behavior whatsoever until you record some. When the code under test invokes a member MockK cannot match to a recorded stub, it throws `io.mockk.MockKException` with a message stating that no answer was found for that call. This applies uniformly — the return type never enters into it. A `Unit` function is, at the JVM level, still a method invocation MockK must answer; the fact that the value it must produce is the singleton `Unit` does not make the stub optional. This surprises people because at the call site `logger.info("x")` looks like it does nothing observable, so "there is nothing to stub" feels true. It isn't. Strictness is a deliberate design: the mock tells you the moment the code under test starts talking to a collaborator you did not think about. ## What `just Runs` actually is `Runs` is a marker object in MockK's DSL, and `just` is an infix function on the stub scope. `every { m.f() } just Runs` is equivalent to registering a constant answer whose value is `Unit`. That is the whole mechanism — there is no interception subtlety, no partial-mocking, no pass-through. Three things follow: - **The real method is never executed.** `just Runs` means "do nothing and return", not "do the real thing". - **The call is still recorded.** Stubbing does not exclude an invocation from the mock's call log, so `verify { m.f() }`, ordering checks and captures all see it. "Runs" describes the return, not the visibility. - **It is an ordinary terminator.** It sits in the same slot as `returns`, `throws` and `answers { }`, so it participates in the normal stub-precedence and re-stubbing rules: a later `every` on the same call takes over. `justRun { m.f() }` is a convenience wrapper that records the call and applies the same answer, letting you skip the `every`/`just Runs` bracketing. Pick one style per codebase; they are interchangeable. ## The alternatives, and when they are better - `every { m.f() } returns Unit` — works, but nobody writes it; `just Runs` exists precisely to make the intent readable. - `every { m.f() } throws IllegalStateException()` — the point people forget: a Unit function is a perfectly good failure injection point. If your service must survive a broken audit sink, stub the Unit call to throw and assert the service still completes. - `every { m.f(any()) } answers { collected += firstArg<String>() }` — when the test wants a cheap recording side effect instead of a capture slot. The block's value must be `Unit`, which it is if the last expression is a Unit expression. - Creating the mock so Unit members are pre-answered — a creation-time option that removes the need for `just Runs` on *every* Unit member at once. That is a mock-creation decision rather than a stubbing one; the tradeoff is the usual strictness tradeoff: fewer lines of setup, but calls you never thought about now pass silently. ## Sequencing and Unit functions Because `just Runs` is just a first answer, a Unit function can also have an evolving behavior, most usefully "work, then start failing": ```kotlin every { sink.publish(any()) } just Runs andThenThrows IOException("broker down") ``` The first publish succeeds, every later one fails — a compact way to test a partially-failing batch. Note that the exhaustion rule applies as usual: the exception, being the last entry, repeats for all subsequent calls. ## Common mistakes The two habitual errors are (1) assuming a stubbed-to-do-nothing call is also invisible to verification — it is not — and (2) reaching for a relaxed or broadly-relaxed mock the first time a Unit call fails, which silences that failure *and* every other unplanned interaction on the mock. Adding one `just Runs` keeps the strictness that made the test fail informatively in the first place. A third, subtler one: `just Runs` on a suspend Unit function must be recorded with `coEvery`, since the call has to be made from a suspending context. The terminator itself is unchanged.

  • If a call is stubbed with just Runs, will verify still see it?
    Yes. Stubbing decides what the call returns; it does not remove the invocation from the mock's recorded-call log. verify, ordered verification and argument capture all observe a just Runs call exactly like any other. Suppressing a call from the record is a separate mechanism, not something a terminator does.
  • Someone fixes a failing Unit call by switching the mock to relaxed instead of adding just Runs. What is your review comment?
    It works, but it changes the mock globally: every unplanned call on that mock now returns a default instead of failing, so the next accidental interaction goes unnoticed. Prefer the targeted stub, and reserve broader relaxation for collaborators with many uninteresting members where you have consciously accepted the loss of strictness.

saying these in an interview costs you the question

  • "Unit methods don't need stubbing because they return nothing."
  • Thinking just Runs invokes the real implementation like a partial mock.
  • Believing a just Runs call is excluded from verification.
  • Reaching for a relaxed mock as the standard fix for one failing Unit call.
  • Not realising a Unit-returning function can be stubbed to throw.

context