skip to content

How do you capture the argument passed to a suspend collaborator and verify the call within a deadline? Explain CapturingSlot with coVerify and the timeout option.

level: seniorimportance: nice to knowfreq 35%

answer

  1. slot<T>() + capture(slot) -> slot.captured
  2. list capture for multiple calls
  3. coVerify(timeout = ms) for async
  4. virtual time: advanceUntilIdle then verify
  5. capture in coEvery feeds coAnswers

basics

~20 s

Use a slot() with capture() in coEvery or coVerify to grab the actual argument, then assert on slot.captured. For async work, coVerify(timeout = ms) waits up to that many milliseconds for the call to happen before failing.

solid answer

~40 s

Create a CapturingSlot with slot<T>(), reference it via capture(slot) inside coEvery (to capture at stub time) or coVerify (to capture at verify time), then assert against slot.captured. For multiple invocations use a mutableListOf<T>() with capture(list). For asynchronous suspend interactions launched on another coroutine, coVerify(timeout = 1000) { ... } polls up to 1000 ms for the expected call, which suits real dispatchers; under runTest's virtual time you usually advanceUntilIdle() instead so verification is deterministic and instant. Combine with counts (exactly, atLeast) and ordering (coVerifyOrder). Capturing in coVerify reads the recorded call after the fact; capturing in coEvery lets coAnswers compute a reply from the captured value. Pitfall: a single slot holds only the last capture, so reaching for a list when the function is called more than once.

code

kotlin · 6 lines
kotlin
val slot = slot<Message>()
runTest {
    service.notifyUser(mailer)
    coVerify(timeout = 1000) { mailer.send(capture(slot)) }
    assertEquals("hello", slot.captured.body)
}

go deeper

for a junior

Knows slot()/capture()/slot.captured grabs the argument passed to a mocked suspend function.

for a middle

Switches to a list capture for multiple calls and combines capture with counts in coVerify.

for a senior

Distinguishes coVerify(timeout) for real dispatchers from advanceUntilIdle under virtual time and captures in coEvery to feed coAnswers.

for a principal

Establishes deterministic async-verification patterns, avoiding wall-clock timeouts in virtual-time tests and standardizing capture conventions across the suite.

## Capturing arguments MockK captures actual arguments into a **`CapturingSlot`** (`slot<T>()`) or a list. ```kotlin interface Mailer { suspend fun send(msg: Message) } val mailer = mockk<Mailer>(relaxed = true) val slot = slot<Message>() runTest { service.notifyUser(mailer) // calls send(...) coVerify { mailer.send(capture(slot)) } // capture at verify time assertEquals("hello", slot.captured.body) } ``` - `capture(slot)` inside `coVerify` records the **actual** argument of the matched call into `slot.captured`. - A single `CapturingSlot` keeps only the **last** captured value. For multiple calls use a list: ```kotlin val msgs = mutableListOf<Message>() coVerify { mailer.send(capture(msgs)) } assertEquals(3, msgs.size) ``` You can also capture inside `coEvery` so a `coAnswers` block can react to the captured value: ```kotlin val slot = slot<Long>() coEvery { repo.load(capture(slot)) } coAnswers { User(id = slot.captured) } ``` ## Timeout for async verification When the suspend interaction happens on a **separate coroutine/real dispatcher**, the call may not have occurred yet when you verify. `coVerify(timeout = millis) { ... }` retries verification for up to `millis` before failing: ```kotlin coVerify(timeout = 1000) { mailer.send(any()) } ``` ### timeout vs virtual time - Under **`runTest`** (virtual time), prefer `advanceUntilIdle()` (or `runCurrent()`), then a plain `coVerify`. This is deterministic and doesn't depend on wall-clock duration. - `timeout` is most useful with **real** dispatchers (e.g., integration-style tests) where you genuinely wait for background work. ## Combining options All mix together: ```kotlin coVerify(exactly = 1, timeout = 500) { mailer.send(capture(slot)) } ``` And ordering for sequences of suspend calls: `coVerifyOrder { ... }` / `coVerifySequence { ... }`. ## Pitfalls - **Single slot, many calls** ⇒ you only see the last argument; use a list. - **Relying on `timeout` under virtual time** ⇒ wastes real time or flakes; advance the scheduler instead. - **Capturing in verify vs stub** ⇒ verify-time capture reads after the fact; stub-time capture lets `coAnswers` compute a dynamic reply. - Type mismatch on `slot<T>()` surfaces as a cast error when read.

  • Your suspend function is called 3 times but slot.captured shows only one value. Why?
    A CapturingSlot retains only the last capture. Use mutableListOf<T>() with capture(list) to collect all invocations.
  • Under runTest, should you use coVerify(timeout = ...) to wait for a launched coroutine?
    Usually no. Advance virtual time with advanceUntilIdle()/runCurrent() and then verify — it's deterministic and instant. timeout is for real dispatchers.

saying these in an interview costs you the question

  • Using one slot for a function called many times and expecting all values
  • Reaching for timeout under runTest instead of advancing virtual time
  • Not knowing capture works in both coEvery and coVerify
  • Reading slot.captured before the call actually happened

context