Introduction to MockK
MockK is a mocking library built specifically for Kotlin. Unlike older Java-oriented mocking frameworks such as Mockito or PowerMock, MockK was designed from the ground up to embrace Kotlin's unique language features — including first-class coroutine support, extension functions, companion objects, data classes, and lambda-heavy APIs. It provides a clean, expressive DSL that feels natural in Kotlin codebases, making it the go‑to choice for unit testing in modern Android, backend, and multiplatform projects.
What Makes MockK Special?
- Kotlin-first design: MockK understands Kotlin's type system, null safety, and function types natively, so you never fight the language when writing tests.
- Coroutine support: MockK provides dedicated constructs like
coEvery,coVerify, andcoAnswersfor suspending functions, removing the need for boilerplate coroutine scaffolding in tests. - Relaxed mocks: By default, mocks can be configured to return sensible defaults (zeros, empty strings, nulls) without explicit stubbing — reducing setup noise for large dependency graphs.
- Extension function mocking: MockK can mock Kotlin extension functions, which are notoriously difficult to handle with Java-based frameworks.
- Argument capturing and advanced matching: It supports rich matchers, capture slots, and lambda argument verification out of the box.
Getting Started with MockK
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Adding MockK to Your Project
For a Gradle-based project, add the following dependency to your build.gradle.kts file. MockK is available on Maven Central, and the latest stable version can be found on the official MockK website or Maven Central. The example below uses a representative version.
// For JVM / Android projects
dependencies {
testImplementation("io.mockk:mockk:1.13.12")
// Optional: For instrumented Android tests (if needed)
androidTestImplementation("io.mockk:mockk-android:1.13.12")
// Optional: For agent-based features like constructor mocking
testImplementation("io.mockk:mockk-agent:1.13.12")
}
For a multiplatform Kotlin project, MockK offers a dedicated artifact that works across JVM, Android, and iOS targets (via Kotlin/Native). Check the official documentation for the most current multiplatform coordinates.
Core Mocking Concepts
Creating Basic Mocks
To create a mock of any class or interface, use mockk<T>(). The mock implements or extends the target type and can be passed wherever the original type is expected.
// Interface we want to mock
interface UserRepository {
fun getUserById(id: String): User?
fun saveUser(user: User): Boolean
}
// Simple mock creation
val userRepo: UserRepository = mockk<UserRepository>()
You can also assign a name to a mock for better error messages in large test suites:
val userRepo = mockk<UserRepository>("userRepositoryMock")
Stubbing Behaviors with every
The core stubbing construct in MockK is the every { ... } returns ... block. Inside the lambda, you call the method you want to stub exactly as you would in production code. MockK records the call pattern and wires the specified return value (or behavior) to it.
// Stub a simple method
val userRepo = mockk<UserRepository>()
every { userRepo.getUserById("user-123") } returns User("Alice", 28)
every { userRepo.getUserById("user-456") } returns null
every { userRepo.saveUser(any()) } returns true
For methods that throw exceptions, use throws instead of returns:
every { userRepo.getUserById("invalid-id") } throws IllegalArgumentException("Invalid ID format")
You can also use answers for dynamic behavior, where the return value depends on the input arguments:
every { userRepo.getUserById(any()) } answers { call ->
val id = call.firstArg<String>()
if (id.startsWith("admin")) User("Admin-$id", 99) else null
}
Verifying Interactions
After the system under test has executed, use verify to assert that specific methods were called with expected arguments and frequencies.
// Exact verification
verify(exactly = 1) { userRepo.getUserById("user-123") }
verify(atLeast = 1) { userRepo.saveUser(any<User>()) }
verify(atMost = 2) { userRepo.getUserById(any()) }
// Verify no interactions with a specific method
verify(exactly = 0) { userRepo.getUserById("never-called-id") }
// Verify all stubbed methods were actually invoked (strict mode check)
verifyAll()
For a more relaxed verification that confirms the calls happened without enforcing strict ordering, use verifyOrder:
verifyOrder {
userRepo.getUserById("user-123")
userRepo.saveUser(any())
}
Argument Matchers
MockK provides a rich set of matchers to define stubbing and verification rules flexibly.
any()— matches any argument of the expected type (non‑null)any<T>()— matches any argument, including nullable typeseq(value)— matches exact equalitymatch<T> { predicate }— custom matching lambdamore(),less(),cmpEq()— comparison matchers for numbersand(),or(),not()— logical combinationsisNull(),notNull()— nullability checks
// Stub with custom matcher
every { userRepo.getUserById(match<String> { it.startsWith("admin") }) } returns User("Admin", 0)
// Verify with combined matchers
verify { userRepo.saveUser(match<User> { it.name == "Alice" && it.age >= 18 }) }
// Numeric matchers
every { calculator.divide(any(), more(0.0)) } returns 42.0
Relaxed Mocks
By default, MockK operates in strict mode: calling a method that has not been explicitly stubbed throws an exception. This is excellent for catching unexpected interactions early. However, in tests where you only care about a subset of interactions, you can use relaxed mocks, which return a sensible default value for unstubbed calls — zeros, empty strings, empty collections, or nulls.
// Relaxed mock returns defaults for unstubbed methods
val relaxedRepo = mockk<UserRepository>(relaxed = true)
// No stubbing needed for getUserById; returns null by default
relaxedRepo.getUserById("any-id") // returns null
// Still stub specific methods when needed
every { relaxedRepo.saveUser(any()) } returns true
Relaxed mocks are especially useful when the system under test interacts with a large dependency, but you only need to verify a few critical calls while letting the rest silently succeed.
Spies and Partial Mocking
A spy wraps a real object and allows you to override specific methods while delegating the rest to the original implementation. This is valuable when you want to test a class that has both complex logic and external dependencies mixed together.
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun subtract(a: Int, b: Int): Int = a - b
fun complexOperation(x: Int): Int {
val step1 = add(x, 10)
return subtract(step1, 5)
}
}
// Create a spy of a real Calculator instance
val realCalc = Calculator()
val spyCalc = spyk(realCalc)
// Override only the add method, keep real subtract
every { spyCalc.add(any(), any()) } returns 999
// Now complexOperation calls the fake add but real subtract
val result = spyCalc.complexOperation(5) // add(5,10) → 999; subtract(999,5) → 994
Be cautious with spies: they can introduce partial state management issues. Prefer pure mocks for external dependencies and refactor code to keep spying minimal.
Mocking Coroutines
MockK's coroutine support is one of its strongest features. Use coEvery, coVerify, coAnswers, and coJustRun as drop‑in replacements for their non‑coroutine counterparts when working with suspend functions.
// Repository with suspending functions
interface CoroutineUserRepository {
suspend fun fetchUser(id: String): User
suspend fun updateUser(user: User): Boolean
}
// Stubbing suspending functions
val repo = mockk<CoroutineUserRepository>()
coEvery { repo.fetchUser("user-1") } returns User("Alice", 28)
coEvery { repo.updateUser(any()) } returns true
// In your test, call the system under test inside a coroutine scope
@Test
fun `test coroutine-based repository`() = runTest {
val service = UserService(repo)
val result = service.refreshUserData("user-1")
coVerify(exactly = 1) { repo.fetchUser("user-1") }
coVerify(atLeast = 1) { repo.updateUser(any()) }
}
For coroutine‑based answers that involve delays or custom suspending logic:
coEvery { repo.fetchUser(any()) } coAnswers {
delay(100) // simulate network latency
User("Delayed-User", 42)
}
MockK also provides coJustRun for stubbing suspend functions that return Unit:
coJustRun { repo.clearCache() }
Capturing Arguments
Sometimes you need to capture arguments passed to a mock to perform detailed assertions later. MockK offers slot and mutableListOf based capturing.
// Capture a single argument using a slot
val userSlot = slot<User>()
val repo = mockk<UserRepository>()
every { repo.saveUser(capture(userSlot)) } returns true
// Invoke the system under test
val service = UserService(repo)
service.registerNewUser("Bob", 35)
// Assert on the captured argument
val capturedUser = userSlot.captured
assertEquals("Bob", capturedUser.name)
assertEquals(35, capturedUser.age)
For capturing multiple arguments across several calls, use a mutable list:
val usersList = mutableListOf<User>()
every { repo.saveUser(capture(usersList)) } returns true
// Multiple calls
service.registerNewUser("Alice", 28)
service.registerNewUser("Charlie", 42)
assertEquals(2, usersList.size)
assertTrue(usersList.any { it.name == "Alice" })
Sequential and Chained Calls
When a method is called multiple times and needs to return different values each time, MockK supports chained stubbing:
// Return different values on each call
every { repo.getUserById("user-1") } returns User("Alice", 28) andThen
User("Alice-updated", 29) andThen
User("Alice-final", 30)
// First call returns Alice(28), second returns Alice-updated(29), third returns Alice-final(30)
You can also mix returns and throws in a chain:
every { repo.getUserById("user-1") } returns User("Valid", 10) andThen
throws RuntimeException("Second call fails") andThen
returns User("Recovered", 10)
Mocking Extension Functions
Kotlin extension functions are resolved statically at compile time, which makes them challenging for traditional reflection‑based mocking frameworks. MockK handles them transparently using its agent or by instrumenting the bytecode.
// Define an extension function on String
fun String.isValidEmail(): Boolean {
return this.contains("@") && this.contains(".")
}
// Mock the extension function
mockkStatic("com.example.ExtensionsKt") // FQN of the file/class containing the extension
every { "test@example.com".isValidEmail() } returns false // override behavior
// Now the system under test sees the mocked behavior
val validator = EmailValidator()
assertFalse(validator.validate("test@example.com"))
// Clean up static mocking after the test
unmockkStatic("com.example.ExtensionsKt")
Mocking Static Methods and Companion Objects
MockK can mock Kotlin companion objects (which compile to static methods on the JVM) as well as Java static methods.
class Logger {
companion object {
fun log(message: String): String {
return "[${System.currentTimeMillis()}] $message"
}
}
}
// Mock the companion object
mockkStatic("com.example.Logger\$Companion") // Note the \$ for inner class
every { Logger.log(any()) } returns "MOCKED-LOG"
// Test
val result = Logger.log("Important message")
assertEquals("MOCKED-LOG", result)
// Clean up
unmockkStatic("com.example.Logger\$Companion")
For Java static methods, the approach is similar — use mockkStatic with the fully qualified class name and call unmockkStatic afterward to avoid cross‑test contamination.
Mocking Constructors
MockK can intercept constructor calls to return mock instances instead of real objects. This requires the MockK agent or the mockk-agent dependency.
// Mock constructor of a concrete class
mockkConstructor(SomeDependency::class)
every { anyConstructed<SomeDependency>().doSomething() } returns "mocked"
// When the system under test internally creates SomeDependency(),
// the mock is returned instead
val service = ServiceUnderTest()
val result = service.execute()
assertEquals("mocked", result)
// Clean up
unmockkConstructor(SomeDependency::class)
Object Mocks
Kotlin object declarations (singletons) can be mocked with mockkObject. This is useful for testing code that depends on global singleton utilities.
object ConfigManager {
fun getSetting(key: String): String = "real-value"
}
// Mock the singleton object
mockkObject(ConfigManager)
every { ConfigManager.getSetting("theme") } returns "dark-mode"
// Test
val theme = ConfigManager.getSetting("theme")
assertEquals("dark-mode", theme)
// Unmock to restore original behavior
unmockkObject(ConfigManager)
Best Practices
- Prefer strict mocks over relaxed mocks: Strict mode (the default) catches unexpected interactions early and forces you to think about which dependencies are actually exercised. Reserve relaxed mocks for integration‑style tests with large, incidental dependency graphs.
- Use meaningful mock names: Assign descriptive names via
mockk<T>("name")to improve clarity in assertion failures and test logs. - Match arguments precisely when possible: Use
eq()or exact values in stubs and verifications. Over‑usingany()can hide bugs where the wrong arguments are passed. - Clean up static, constructor, and object mocks: Always call the corresponding
unmockkStatic,unmockkConstructor, orunmockkObjectin a@AfterortearDownblock to prevent test pollution. - Keep coroutine tests simple: Use
runTestfromkotlinx.coroutines.testand pair it with MockK'scoEvery/coVerify. Avoid mixingrunBlockingwith MockK coroutine stubs — it can lead to unexpected blocking behavior. - Verify only what matters: Over‑verification makes tests brittle. Verify critical interactions and use
verifyAllsparingly, only when strict behavioral contracts are essential. - Favor composition over spying: If you find yourself heavily spying on a class, consider refactoring it into smaller, testable components with explicit dependencies. Spies are powerful but can mask design issues.
- Keep stubs focused: Each test should stub only the methods it needs. Extracting common stubbing into helper functions or using
@Beforeblocks can reduce duplication, but be mindful of shared state between tests.
Common Pitfalls and How to Avoid Them
- Stubbing a method but never calling it: In strict mode, MockK throws an exception if a stubbed method is never invoked. If this is intentional (e.g., a fallback path), either remove the stub or switch to a relaxed mock for that specific test.
- Mismatched argument matchers: Using
any()in a stub buteq()in verification (or vice versa) can cause confusing mismatches. Keep matchers consistent, or use explicit matchers in both places. - Forgetting coroutine scope in tests: Calling
coEverystubs outside a coroutine context works fine, but actually invoking the stubbed suspend function requires a coroutine scope. Wrap test logic inrunTestorrunBlocking. - Leaking static mocks across tests: Static, constructor, and object mocks are global and persist until explicitly cleared. A leaked mock can cause unrelated tests to fail with cryptic errors. Always clean up in
tearDown. - Mocking final or private methods without agent: MockK can mock final methods on JVM with the agent, but without it, some Kotlin‑specific constructs may not be mockable. Ensure the agent dependency is configured if you need deep mocking capabilities.
Conclusion
MockK has earned its place as the standard mocking library for Kotlin development by respecting the language's idioms and providing a fluent, readable API that scales from simple unit tests to complex coroutine‑based integration scenarios. Its support for suspending functions, extension functions, companion objects, and argument capturing removes the friction that developers often experience when using Java‑centric frameworks in Kotlin projects. By adopting the practices outlined in this guide — preferring strict mocks, cleaning up global state, matching arguments precisely, and leveraging coroutine‑aware stubs — you can build a fast, reliable, and maintainable test suite that keeps your Kotlin codebase healthy as it grows.