MockK for Coroutines: coEvery, coVerify, Flow, and Static/Object Mocks
Stub and verify suspend functions and Flows the idiomatic way — coEvery and coVerify do for coroutines what every and verify do for blocking code.
Explore the full explanation 8 ideas
MockK is the de facto mocking library for Kotlin, and its coroutine support is what makes it indispensable in a Spring Boot 3 reactive or suspending codebase. The core idea is symmetry: for ordinary functions you stub behavior with `every { }` and assert calls with `verify { }`, while for `suspend` functions you use the coroutine-aware twins `coEvery { }` and `coVerify { }`. These `co`-prefixed builders run their lambda inside a coroutine context so they can legally call suspend functions, which a plain `every { }` block cannot. Reaching for `every` on a suspend function is the single most common MockK-coroutines mistake and produces a compile error or a stub that never matches.
Suppose you have a `UserRepository` exposing `suspend fun findById(id: Long): User?`. You create a mock with `mockk<UserRepository>()` and then teach it how to respond: `coEvery { repo.findById(1L) } returns User(1L, "Ada")`. The right-hand side accepts the same operators as `every` — `returns`, `returnsMany`, `throws`, and `coAnswers { }` for dynamic responses that themselves may suspend. To simulate latency or a timeout you can write `coEvery { repo.findById(any()) } coAnswers { delay(100); null }`. After exercising the system under test you confirm the interaction with `coVerify { repo.findById(1L) }`, optionally tightening it with `exactly = 1`, `atLeast`, or `timeout` parameters.
Verification ordering and call counts work identically to the blocking API. `coVerify(exactly = 0) { repo.deleteById(any()) }` proves a destructive call never happened, while `coVerifyOrder { repo.lock(id); repo.findById(id); repo.unlock(id) }` asserts a precise sequence. Because your test body itself calls suspend functions on the system under test, wrap the whole test in `runTest { }` from `kotlinx-coroutines-test`. `runTest` provides a `TestScope` with a virtual clock, so any `delay` inside your `coAnswers` stubs is skipped instantly rather than blocking the test thread — your suite stays fast even when the production code waits.
Flow-returning functions deserve special attention. A function like `fun streamEvents(): Flow<Event>` is *not* a suspend function — it returns a cold `Flow` synchronously — so you stub it with ordinary `every { }`, not `coEvery { }`. Return a real flow built with `flowOf(a, b, c)` or the `flow { }` builder when you need to emit conditionally, suspend between emissions, or throw mid-stream. For example `every { repo.streamEvents() } returns flowOf(Event("created"), Event("updated"))` gives the collector two values and then completes. To assert what was collected, drain the flow inside `runTest` with `.toList()` and compare. Only mark the stub with `coEvery` if the factory itself is declared `suspend fun streamEvents(): Flow<Event>`.
Sometimes the dependency you must control is not an injected instance but a top-level or extension function, a companion-object member, or a Kotlin `object` singleton. For these MockK offers `mockkStatic`, `mockkObject`, and `mockkConstructor`. `mockkStatic("com.example.TimeUtilsKt")` (note the `Kt` suffix MockK uses for file-level functions) lets you stub a free function such as `now()`; `mockkObject(FeatureFlags)` turns a real singleton into a spy you can selectively override while leaving untouched members intact. Both pair naturally with coroutines — `coEvery { SomeObject.fetch() } returns data` works once the object is mocked.
The crucial discipline with static and object mocks is cleanup, because they patch global state that leaks across tests. Always undo them: call `unmockkStatic(...)` / `unmockkObject(...)` in an `@AfterEach`, or wrap the mocked region in `mockkStatic(...) { ... }` / `mockkObject(...) { ... }`, the block form that auto-unmocks on exit. A blanket `unmockkAll()` in tear-down is a pragmatic safety net for larger suites. Forgetting this is the second classic pitfall: a leaked `mockkObject(FeatureFlags)` silently corrupts every later test that touches the singleton, producing maddening order-dependent failures.
Putting it together for a typical Spring service test: annotate the class, declare `private val repo = mockk<UserRepository>()`, construct `UserService(repo)` by hand (constructor injection makes manual wiring trivial and avoids a Spring context), and write each test inside `runTest`. Use `coEvery` to arrange suspend stubs, exercise the service, then `coVerify` the outbound calls. Relax MockK strictness only where it helps: `mockk<UserRepository>(relaxed = true)` auto-stubs every function with sensible defaults (zero, empty collections, empty flows), which is handy when you only care about a couple of interactions, while `relaxUnitFun = true` relaxes only `Unit`-returning functions and keeps you honest about the rest.
A few final idioms keep coroutine tests robust. Prefer argument matchers — `any()`, `eq()`, `match { it.id > 0 }` — over hard-coded values when the exact input is incidental. Capture arguments for richer assertions with `val slot = slot<User>(); coEvery { repo.save(capture(slot)) } returns Unit`, then inspect `slot.captured`. When a suspend stub must vary by call, `coAnswers { firstArg<Long>().let { id -> store[id] } }` reads the actual arguments. And remember that `coVerify` can also assert *presence over time* with `coVerify(timeout = 500) { repo.flush() }`, which waits up to half a second for an asynchronously-triggered call before failing — invaluable for fire-and-forget coroutines launched in a background scope.
Let’s figure it out.
Ask me about this course or where to start.
import io.mockk.coEveryimport io.mockk.coVerifyimport io.mockk.mockkimport kotlinx.coroutines.delayimport kotlinx.coroutines.test.runTestimport kotlin.test.Testimport kotlin.test.assertEqualsdata class User(val id: Long, val name: String)interface UserRepository {suspend fun findById(id: Long): User?suspend fun deleteById(id: Long)}class UserService(private val repo: UserRepository) {suspend fun displayName(id: Long): String =repo.findById(id)?.name ?: "unknown"}class UserServiceTest {private val repo = mockk<UserRepository>()private val service = UserService(repo)@Testfun `returns name from repository`() = runTest {// coEvery for suspend functions; coAnswers can itself suspendcoEvery { repo.findById(1L) } coAnswers { delay(50); User(1L, "Ada") }val name = service.displayName(1L)assertEquals("Ada", name)coVerify(exactly = 1) { repo.findById(1L) }coVerify(exactly = 0) { repo.deleteById(any()) }}}
Stubbing and verifying a suspend function. coEvery arranges the stub, runTest hosts the suspend calls, and coVerify asserts the interaction. Runs on plain stdlib + kotlinx.coroutines + MockK (no Spring).
import io.mockk.everyimport io.mockk.mockkimport kotlinx.coroutines.flow.Flowimport kotlinx.coroutines.flow.flowOfimport kotlinx.coroutines.flow.mapimport kotlinx.coroutines.flow.toListimport kotlinx.coroutines.test.runTestimport kotlin.test.Testimport kotlin.test.assertEqualsdata class Event(val type: String)interface EventRepository {fun streamEvents(): Flow<Event> // cold flow, returned synchronously}class EventReporter(private val repo: EventRepository) {fun titles(): Flow<String> = repo.streamEvents().map { it.type.uppercase() }}class EventReporterTest {private val repo = mockk<EventRepository>()@Testfun `transforms emitted events`() = runTest {// every (not coEvery): the factory is not a suspend functionevery { repo.streamEvents() } returns flowOf(Event("created"), Event("updated"))val result = EventReporter(repo).titles().toList()assertEquals(listOf("CREATED", "UPDATED"), result)}}
Mocking a Flow-returning function. streamEvents() is NOT suspend, so use plain every and return a real cold flow; collect with toList() inside runTest.
import io.mockk.coEveryimport io.mockk.everyimport io.mockk.mockkObjectimport io.mockk.mockkStaticimport kotlinx.coroutines.test.runTestimport kotlin.test.Testimport kotlin.test.assertEquals// Top-level function lives in TimeUtils.kt -> MockK class name "TimeUtilsKt"fun nowMillis(): Long = System.currentTimeMillis()object FeatureFlags {suspend fun isEnabled(key: String): Boolean = false // real impl hits a remote store}class StaticAndObjectMockTest {@Testfun `stub top-level fun and object singleton`() = runTest {// Block form: automatically unmocked when the lambda returnsmockkStatic(::nowMillis) {every { nowMillis() } returns 1_700_000_000_000LassertEquals(1_700_000_000_000L, nowMillis())}mockkObject(FeatureFlags) {coEvery { FeatureFlags.isEnabled("beta") } returns trueassertEquals(true, FeatureFlags.isEnabled("beta"))}// After the blocks, FeatureFlags and nowMillis are restored.}}
mockkStatic for a top-level function and mockkObject for a singleton, both with coroutine stubs. Note the block forms auto-unmock on exit, preventing global-state leakage.
import io.mockk.coEveryimport io.mockk.coVerifyOrderimport io.mockk.mockkimport io.mockk.slotimport kotlinx.coroutines.test.runTestimport kotlin.test.Testimport kotlin.test.assertEqualsinterface Store {suspend fun lock(id: Long)suspend fun read(id: Long): Intsuspend fun unlock(id: Long)}class Counter(private val store: Store) {suspend fun readUnderLock(id: Long): Int {store.lock(id)return try { store.read(id) } finally { store.unlock(id) }}}class CounterTest {private val store = mockk<Store>(relaxUnitFun = true)@Testfun `locks around the read in order`() = runTest {val idSlot = slot<Long>()// coAnswers reads the actual argument captured at call timecoEvery { store.read(capture(idSlot)) } coAnswers { (idSlot.captured * 10).toInt() }val value = Counter(store).readUnderLock(7L)assertEquals(70, value)coVerifyOrder {store.lock(7L)store.read(7L)store.unlock(7L)}}}
A self-contained runnable demonstration of coEvery + coAnswers reading real arguments and coVerify ordering. Uses only kotlinx.coroutines + MockK, no framework.
🧠 Check your understanding
0/1 · 0/1 answered1. You need to mock `fun streamEvents(): Flow<Event>` (a regular, non-suspend function that returns a cold Flow) and also `suspend fun findById(id: Long): User?`. Which pairing is correct?