skip to content

Walk through stubbing and verifying a suspend repository call in a runTest block. What's the difference between coVerify and verify, and what happens if you use the wrong one?

level: middleimportance: must knowfreq 70%

answer

  1. coEvery stub, coVerify check
  2. verify block is non-suspend
  3. exactly/atLeast/atMost work on coVerify
  4. coVerifyOrder / coVerifySequence
  5. advanceUntilIdle for delayed calls

basics

~20 s

Use coEvery to stub the suspend call and coVerify to check it ran, all inside runTest. verify can't call a suspend function in its block, so it won't compile for suspend functions — you must use coVerify.

solid answer

~40 s

Stub with coEvery { repo.fetch(id) } returns dto, exercise the system under test inside runTest, then assert the interaction with coVerify { repo.fetch(id) }. coVerify's block is a suspend lambda, so it can reference the suspend call; verify's block is not, so verify { repo.fetch(id) } fails to compile because fetch is suspend. Both share the same verification machinery: argument matchers (eq, any()), counts (exactly = 1, atLeast, atMost), timeout, and ordering modifiers (coVerifyOrder, coVerifySequence). A common mistake is calling the production code outside a coroutine; without runTest (or another coroutine builder) you can't invoke the suspend function under test at all. coVerify also integrates with the virtual time of runTest, so delayed suspend interactions are observable after advancing time.

code

kotlin · 7 lines
kotlin
@Test fun flow() = runTest {
    val repo = mockk<Repo>()
    coEvery { repo.fetch(1) } returns Dto("Hi")
    Service(repo).titleOf(1)
    coVerify(exactly = 1) { repo.fetch(1) }
    coVerify(exactly = 0) { repo.fetch(2) }
}

go deeper

for a junior

Can write the coEvery + coVerify pair inside runTest for a simple call.

for a middle

Explains the suspend-lambda reason verify fails to compile and uses counts/matchers (exactly, any) on coVerify.

for a senior

Knows the ordering co-variants and diagnoses missed verifications caused by unadvanced virtual time or child coroutines.

for a principal

Designs the test so interaction verification is deterministic under virtual time and reasons about coVerify timeout vs advanceUntilIdle trade-offs.

## End-to-end flow ```kotlin class Service(private val repo: Repo) { suspend fun titleOf(id: Long): String = repo.fetch(id).title } interface Repo { suspend fun fetch(id: Long): Dto } @Test fun returnsTitle() = runTest { // 1. coroutine scope val repo = mockk<Repo>() coEvery { repo.fetch(1) } returns Dto("Hi") // 2. stub suspend fun val service = Service(repo) val title = service.titleOf(1) // 3. exercise (suspends) assertEquals("Hi", title) // 4. assert state coVerify(exactly = 1) { repo.fetch(1) } // 5. assert interaction } ``` ## coVerify vs verify - **`verify { }`** — block is a non-suspend lambda. Referencing a `suspend` call inside it does not compile. - **`coVerify { }`** — block is a `suspend` lambda, so it can reference suspend calls. They are otherwise the same verification engine. All the usual options apply to both: - Counts: `coVerify(exactly = 2) { ... }`, `atLeast`, `atMost`. - Negative: `coVerify(exactly = 0) { ... }` to assert *not* called. - Matchers: `coVerify { repo.fetch(any()) }`, `eq(...)`, `match { it > 0 }`. - Timeout: `coVerify(timeout = 1000) { ... }` waits up to N ms for async work. ## Ordering variants For suspend calls, the ordered checks are also co-prefixed: - `coVerifyOrder { ... }` — relative order, gaps allowed. - `coVerifySequence { ... }` — exact sequence, no other calls. - `coVerifyAll { ... }` — all listed happened, any order. ## What goes wrong with the wrong one 1. `verify { repo.fetch(1) }` on a suspend `fetch` — **compile error**. 2. Calling `service.titleOf(1)` outside `runTest` — **compile error** (suspend call outside coroutine). 3. Using a delay-based interaction but never advancing virtual time — the verify may see fewer calls than expected; advance with `advanceUntilIdle()`. ## Mental model Think "if the mocked function has `suspend`, every interaction API gains a `co` prefix." The arguments, counts, and matchers don't change.

  • How do you assert a suspend function was NOT called?
    coVerify(exactly = 0) { repo.fetch(any()) } — same count API as verify, just co-prefixed.
  • Your suspend interaction happens inside a launched child coroutine with a delay. Verify says 0 calls — why?
    Virtual time hasn't advanced, so the child hasn't run yet. Call advanceUntilIdle() (or use coVerify(timeout = ...)) before verifying.

saying these in an interview costs you the question

  • Using verify for a suspend function and claiming it compiles
  • Forgetting runTest and trying to call the suspend SUT directly
  • Thinking counts/matchers differ between verify and coVerify
  • Verifying delayed work without advancing virtual time

context