← Back to DevBytes

MockK: Complete Testing Guide for Developers

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?

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.

// 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

Common Pitfalls and How to Avoid Them

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles