Skip to Content
Course content

155: Mocking with MockK In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a teammate who had spent nearly an entire Tuesday fighting with a unit test for a payment processing flow. He had written a perfectly valid UserRepository mock, but his tests were crashing with a MockKException: no answer found for.... The confusing part? He had clearly defined the behavior using every { ... }. It took us twenty minutes of staring at the code to realize he was calling a suspend function inside a coroutine scope. In MockK, every is for synchronous calls; for suspending functions, you need coEvery. It's a tiny difference in naming, but it's one of those things that can make you want to throw your laptop out the window if you don't know it's happening.

Taming Coroutines with coEvery and coVerify

Since we're working in Kotlin, your services are likely riddled with suspend functions. When you're mocking these, you have to tell MockK to handle the continuation mechanism that powers coroutines. If you use every on a suspending function, you'll get a runtime crash because MockK doesn't know how to "suspend" the mock's response.

Instead, use coEvery to define the stub and coVerify to check if the call actually happened. It looks and feels exactly like the synchronous version, just with a "co" prefix. I usually recommend being explicit here—don't try to find a way around it with runBlocking inside the mock definition; just use the dedicated coroutine tools MockK provides.

val paymentGateway = mockk<PaymentGateway>()
// This would fail for a suspend function:
// every { paymentGateway.process(any()) } returns true 

// This is the correct way:
coEvery { paymentGateway.process(any()) } returns true

// And verifying it:
coVerify { paymentGateway.process(paymentRequest) }

Capturing Arguments with Slots

Sometimes, simply verifying that a function was called isn't enough. You might need to inspect the exact object that was passed to a dependency to ensure your business logic transformed the data correctly before sending it off. This is where slot comes in. A slot acts like a container that "catches" the argument passed during the execution of the test.

I find slots far more readable than using complex match { ... } blocks inside the verify call. You capture the value first, and then you perform your assertions on the captured object using your favorite assertion library. It keeps the "mocking" phase and the "assertion" phase of your test cleanly separated.

val slot = slot<OrderRequest>()
val orderService = mockk<OrderService>()

coEvery { orderService.submitOrder(capture(slot)) } returns OrderResponse(success = true)

// ... call the code that triggers the order submission ...

assertEquals("Expected-Product-ID", slot.captured.productId)
assertEquals(2, slot.captured.quantity)

Balancing Strictness with Relaxed Mocks

By default, MockK is "strict." If your code calls a method on a mock that you haven't explicitly stubbed with every, MockK throws an exception. While this is great for catching unexpected side effects, it can become a nightmare when you're mocking a huge interface where your test only cares about one or two methods, but the class under test calls five other "utility" methods on that same mock.

You can use relaxed = true when creating the mock. A relaxed mock provides default values for all functions (empty strings, zero, or nulls for nullable types) without requiring you to stub every single call. Just be careful: if you relax everything, you might miss the fact that your code is calling a dependency it shouldn't be. I typically use relaxed mocks for "logger" or "analytics" dependencies—things that the system needs to function but aren't central to the logic being tested.

// No need to stub every single log call now
val logger = mockk<AppLogger>(relaxed = true)
val service = MyService(logger)

service.doWork() 

// The test won't crash even if doWork() calls logger.info() five times
verify { logger.info(any()) }



📋 Practical Task

Exercise: Testing the EmailNotificationDispatcher

You have a NotificationDispatcher class that depends on an EmailClient. The EmailClient has a suspending function sendEmail(email: String, body: String).

Your task is to write a test that ensures the NotificationDispatcher correctly formats the email body before sending it. The dispatcher should take a user's name and a message, and format the body as: "Hello [name], your message is: [message]".

Requirements:

  • Use mockk to create a mock of EmailClient.
  • Use coEvery to stub the sendEmail function to return true.
  • Use a slot to capture the body argument passed to sendEmail.
  • Assert that the captured body matches the expected formatted string.
  • Use coVerify to ensure sendEmail was called exactly once.
// Provided classes for your test:
class EmailClient {
    suspend fun sendEmail(email: String, body: String): Boolean = true 
}

class NotificationDispatcher(private val client: EmailClient) {
    suspend fun dispatchNotification(email: String, name: String, msg: String) {
        val formattedBody = "Hello $name, your message is: $msg"
        client.sendEmail(email, formattedBody)
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.