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.
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.
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?