skip to content

You need to assert that a collaborator was called from a background thread or from a coroutine launched on a real dispatcher. How do MockK's verify(timeout = ...) and coVerify(timeout = ...) make that reliable, and what situation makes the verification burn the whole timeout and fail even though the production code is correct?

level: seniorimportance: should knowfreq 28%

answer

  1. calls recorded per mock, not per thread
  2. coVerify(timeout=) for suspend collaborators
  3. blocking wait -> starves same-thread execution
  4. duration == timeout exactly = starvation, not a race
  5. worker exceptions never reach the test

basics

~20 s

Calls from any thread are recorded on the same mock, so MockK can poll for them from the test thread; coVerify does the same for suspend calls. It fails wrongly when the pending work can only run on the very thread MockK is blocking, because that work never gets to execute.

solid answer

~50 s

A mock records every call made on it regardless of which thread made it, so the test thread can observe work done elsewhere. `verify(timeout = ms)` re-evaluates the block until the recorded calls satisfy it; `coVerify(timeout = ms)` is the suspend-function equivalent. That is what makes an assertion against an executor-driven or `Dispatchers.IO`-driven path deterministic without sleeping. The failure mode is self-inflicted starvation: MockK's wait **blocks the calling thread**. If the outstanding work can only progress on that same thread — a single-threaded executor whose worker is the test thread, a dispatcher confined to the test thread, or a scheduler the test itself is supposed to drive — then blocking guarantees the call never happens. You pay the entire timeout and get "no matching calls". The other trap: exceptions on the worker thread do not propagate, so the timeout failure is all you see.

code

kotlin · 6 lines
kotlin
val repo = mockk<OrderRepository>(relaxed = true)
val service = OrderService(repo, CoroutineScope(Dispatchers.IO))

service.submitAsync(order) // launches and returns

coVerify(timeout = 2000) { repo.save(match { it.id == order.id }) }

go deeper

for a junior

Know that async assertions need verify(timeout = ...) or coVerify(timeout = ...) rather than a sleep, and that the mock records calls made on any thread.

for a middle

Explain the polling-against-the-shared-record model and pick the right entry point for suspend versus blocking collaborators.

for a senior

Diagnose the two real failure modes — same-thread starvation and a silently dead worker — from the shape of the failure, and instrument tests so the true cause is visible.

for a principal

Set the expectation that concurrency in production code carries an injectable execution seam, so most tests never need a wall-clock wait at all.

## Why polling works at all A MockK mock keeps a record of the calls made on it. That record belongs to the mock instance, not to the thread that made the call, so a callback invoked on a worker thread lands in the same place a call from the test thread would. Timeout verification exploits exactly this: the test thread repeatedly re-evaluates the verification block against that shared record until it is satisfied or the deadline expires. No cooperation from the production code is required — you do not need a hook, a callback, or an idle signal, only a mock the code eventually touches. ## The two entry points ``` // callback delivered on an executor thread verify(timeout = 2000) { listener.onFinished(file) } // suspend collaborator called from a coroutine on a real dispatcher coVerify(timeout = 2000) { repository.save(any()) } ``` `coVerify` exists because MockK records suspend calls through a separate path; the `timeout` parameter means the same thing on both, and both block the caller while they wait. ## The starvation trap The crucial mechanical fact is that MockK's wait is a **blocking wait on the calling thread**. It does not yield to a scheduler, it does not pump a queue, it does not advance anything. So the wait only works if the outstanding work is being carried by *some other* execution resource. The cases where that is not true look like this: * The production code submits work to an executor that the test wired up to run tasks on the caller's thread (a direct or same-thread executor). Blocking the caller means the task queue is never drained. * The coroutine is launched on a dispatcher that is confined to the test thread, or on a scheduler whose tasks only run when the test itself drives it. Blocking the test thread stops any of it from progressing. * The work is queued behind the very call the test is still inside — a single worker already occupied by something the test must return from first. In all of these, the symptom is identical and confusing: the assertion always takes exactly the timeout and then reports that the call never happened, while the production code is perfectly fine. The tell is that the test duration equals the timeout precisely — a real async race would sometimes pass. When the execution is confined to the test thread, the fix is not a longer timeout; it is to let the work run (drive the executor/scheduler explicitly, or run the code on a genuinely separate thread) and then verify without waiting at all. ## Silent worker failures The second trap is diagnostic rather than mechanical. When code running on a background thread throws, nothing propagates into the test thread; the JVM's default handling logs it or drops it. Your timeout verify then fails with MockK's normal "no matching calls" message and lists what *was* recorded. That message never says "the worker died". So when a timeout verification fails, the first hypothesis should be "the work crashed before reaching this call", not "the timeout was too short". Practical mitigations: give the executor an uncaught-exception handler that fails the test, or stub the collaborator with `answers { ... }` that records enough context to tell you how far execution got. ## Ordering and multiple waits Only the calls named in the block are known to have happened when `verify` returns; other in-flight work is still in flight. Two consequences follow. First, chain waits deliberately — wait for the call that means "the pipeline finished", then assert everything else with plain verifies. Second, remember that `verifyOrder` and `verifySequence` take no timeout; if you need ordered verification with waiting, call `verify` itself and pass both `ordering` and `timeout`. ## How to choose the deadline Pick a value that comfortably exceeds the worst thread-scheduling latency on your slowest CI agent — seconds, not tens of milliseconds. Because the loop returns as soon as the call is recorded, a generous deadline costs nothing on green runs and only shows up on genuine failures. Tightening timeouts to speed up a suite is optimising the one case that is already fast, and buying flakiness with the saving.

  • A timeout verify fails after exactly the full deadline on every single run. What is your first hypothesis?
    Not a too-short timeout — a consistent, exact-duration failure is the signature of work that cannot progress or never started. Either the pending task needs the thread MockK is blocking, or the worker threw before reaching the call. Check whether execution is confined to the caller's thread, and capture uncaught exceptions from the worker before touching the timeout value.
  • When the collaborator is a suspend function, why is coVerify needed rather than plain verify?
    MockK records suspend calls through its coroutine-aware path, so the suspend variants of the DSL are the ones that can express and match them; `coVerify` also lets the verification block contain suspend calls. The `timeout` parameter behaves identically — it bounds a blocking wait on the calling thread while the recorded calls are re-checked.

Polling the mock is watching the mailbox from your window. It works if a postman is out walking; it fails absurdly if you are the postman and you are standing at the window waiting.

saying these in an interview costs you the question

  • Reaching for a longer timeout when the failure duration is always exactly the timeout
  • Believing MockK's wait yields to the scheduler or drives pending tasks
  • Assuming an exception thrown on a worker thread fails the test
  • Thinking calls made on other threads are invisible to the test thread's verify
  • Passing timeout to verifySequence to wait for an ordered async sequence

context