Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
57: Mocking with MockK
I see this all the time when developers first move from older Java mocking frameworks to MockK: they assume that calling mockk<MyService>() creates a "dumb" object that will just return null or a default value when a method is called. They write their test, hit run, and are immediately greeted by a MockKException: no answer found for...
Here is the concrete reason why that's wrong. In MockK, mocks are strict by default. If you haven't explicitly told the mock how to behave for a specific call, it won't guess and it won't return a default; it will crash your test. This is actually a feature, not a bug. It forces you to be intentional about exactly what dependencies are being touched during your test execution.
The "It Just Works" Fallacy vs. Explicit Stubbing
Imagine we're building a PaymentProcessor that depends on an external BankApi. You might be tempted to just mock the API and call your process method:
val bankApi = mockk<BankApi>()
val processor = PaymentProcessor(bankApi)
// This will throw MockKException because we didn't define the behavior!
processor.chargeCustomer("cust_123", 50.0)
To fix this, you have to use every { ... } returns .... This is where you define the "stub"—the predetermined response for a specific input. I always recommend being as specific as possible with your arguments to ensure your logic is actually passing the right data to the dependency.
every { bankApi.authorizePayment("cust_123", 50.0) } returns PaymentResponse(success = true)
// Now this works perfectly
processor.chargeCustomer("cust_123", 50.0)
If you truly don't care about the return value and just want the mock to stop complaining, you can use mockk<BankApi>(relaxed = true). But be careful. I've seen "relaxed" mocks hide bugs for weeks because a developer didn't realize their code was calling a dependency with the wrong arguments, and the relaxed mock just quietly returned a default value instead of failing.
Handling Coroutines with coEvery
Since we're working in Kotlin, you're almost certainly using suspend functions. A common mistake is trying to use every on a suspend function, which will result in a compiler error. For coroutines, you need the "co" prefix: coEvery.
It works exactly the same way, but it's designed to handle the continuation mechanism under the hood of Kotlin coroutines. If the function you're mocking is suspend, coEvery is your only option.
coEvery { bankApi.fetchAccountBalance("cust_123") } returns 1000.0
Verifying Side Effects with verify
Stubbing (telling the mock what to return) is only half the battle. Sometimes, a function doesn't return anything, but it's supposed to do something—like sending an email or logging an error. This is where verify comes in.
I like to think of verify as a way of asking the mock: "Did this actually happen?"
// Act
processor.chargeCustomer("cust_123", 50.0)
// Assert: Ensure the bank API was actually called exactly once
verify(exactly = 1) { bankApi.authorizePayment("cust_123", 50.0) }
You can also use verify(exactly = 0) to ensure a specific path was not taken, which is incredibly useful for testing error handling or validation logic where the expensive API call should be skipped.
📋 Practical Task
Exercise: Testing the UserNotificationDispatcher
You are tasked with testing a UserNotificationDispatcher class. This class has a dependency on a UserPreferencesRepository (to check if the user has notifications enabled) and an EmailClient (to actually send the message).
The Scenario:
The dispatchNotification(userId: String, message: String) method should:
1. Call UserPreferencesRepository.isNotificationsEnabled(userId).
2. If true, call EmailClient.sendEmail(userId, message).
3. If false, it should NOT call the EmailClient.
Your Task: Write two tests using MockK:
- Success Case: Mock the repository to return
trueand verify thatsendEmailwas called exactly once with the correct arguments. - Disabled Case: Mock the repository to return
falseand verify thatsendEmailwas never called.
Starter Code:
class UserNotificationDispatcher(
private val prefsRepo: UserPreferencesRepository,
private val emailClient: EmailClient
) {
fun dispatchNotification(userId: String, message: String) {
if (prefsRepo.isNotificationsEnabled(userId)) {
emailClient.sendEmail(userId, message)
}
}
}
interface UserPreferencesRepository {
fun isNotificationsEnabled(userId: String): Boolean
}
interface EmailClient {
fun sendEmail(userId: String, message: String)
}There are no comments for now.