MockK Guide: Mocking in Kotlin with mockk

MockK Guide: Mocking in Kotlin with mockk

MockK is the go-to mocking library for Kotlin. Unlike Mockito (which was designed for Java), MockK understands Kotlin's type system — it handles data class, object, companion object, extension functions, coroutines, and final classes without workarounds.

Installation

// build.gradle.kts
testImplementation("io.mockk:mockk:1.13.12")

For Android:

testImplementation("io.mockk:mockk-android:1.13.12")

Creating Mocks

import io.mockk.mockk

val repository = mockk<UserRepository>()
val service = mockk<EmailService>()

mockk<T>() creates a strict mock — every call must be stubbed. Unstubbed calls throw MockKException.

Relaxed Mocks

A relaxed mock returns sensible defaults (0, "", null, empty collections) for unstubbed calls:

val logger = mockk<Logger>(relaxed = true)
// No stubs needed — calls return defaults without throwing
logger.log("anything")

Use relaxed mocks for dependencies you don't care about in a particular test (like loggers, analytics), strict mocks for dependencies whose behavior you're testing.

Stubbing with every

every { repository.findById(1) } returns User(id = 1, name = "Alice")
every { repository.findById(2) } returns null
every { service.send(any()) } returns Unit

Argument Matchers

every { repository.findById(any()) } returns null         // match any argument
every { repository.findById(eq(5)) } returns user         // exact match
every { repository.search(match { it.length > 3 }) } returns listOf()  // predicate
every { cache.get(or(eq("key1"), eq("key2"))) } returns "value"

Answers

answers gives you access to the actual argument values:

every { repository.findById(any()) } answers {
    val id = firstArg<Long>()
    if (id > 0) User(id, "User $id") else null
}

firstArg(), secondArg(), lastArg(), and arg<T>(index) extract call arguments.

Throwing Exceptions

every { repository.findById(-1) } throws IllegalArgumentException("invalid id")
every { service.send(any()) } throwsMany listOf(
    IOException("timeout"),
    IOException("timeout"),
    Unit   // third call succeeds
)

Verifying Calls

verify { repository.findById(1) }               // called at least once
verify(exactly = 1) { repository.save(any()) }  // called exactly once
verify(exactly = 0) { service.send(any()) }     // never called
verify(atLeast = 2) { cache.invalidate(any()) }
verify(atMost = 3) { logger.debug(any()) }

verifyOrder

verifyOrder {
    repository.findById(1)
    repository.save(user)
    service.send("user@example.com")
}

confirmVerified

Asserts that every interaction on a mock was explicitly verified:

confirmVerified(repository, service)

Useful for catching unexpected calls.

Slot Capture

Capture arguments passed to mocks for inspection:

val slot = slot<User>()

every { repository.save(capture(slot)) } returns Unit

userService.createUser("Alice", 25)

assertEquals("Alice", slot.captured.name)
assertEquals(25, slot.captured.age)

For multiple captures:

val emails = mutableListOf<String>()

every { service.send(capture(emails)) } returns Unit

notifier.notifyAll(users)

assertEquals(users.size, emails.size)
assertTrue(emails.all { it.contains("@") })

Spy

A spy wraps a real object, delegating to the real implementation by default:

val realService = UserService(repository)
val spy = spyk(realService)

// Override specific methods
every { spy.validate(any()) } returns true

// Real method calls for everything else
spy.createUser("Alice")

verify { spy.createUser("Alice") }

Use spies when you want to test a real implementation but stub or verify specific internal calls.

Mocking Companion Objects and Objects

MockK can mock Kotlin object and companion objects:

object Config {
    fun getApiKey(): String = System.getenv("API_KEY") ?: ""
}

mockkObject(Config)
every { Config.getApiKey() } returns "test-key"

// Always unmock after the test
unmockkObject(Config)

Or use mockkObject with a block:

mockkObject(Config) {
    every { Config.getApiKey() } returns "test-key"
    // test code here
}
// Config is automatically unmocked after the block

Mocking Extension Functions

// Defined in utils.kt
fun String.sanitize(): String = this.trim().lowercase()

mockkStatic("utils.UtilsKt")
every { any<String>().sanitize() } returns "sanitized"

Coroutine Mocking

MockK supports suspend functions natively:

import io.mockk.coEvery
import io.mockk.coVerify

val repository = mockk<UserRepository>()

coEvery { repository.findByIdAsync(1) } returns User(1, "Alice")
coEvery { repository.findByIdAsync(999) } throws NotFoundException()

// Verify suspend calls
coVerify { repository.findByIdAsync(1) }
coVerify(exactly = 0) { repository.findByIdAsync(999) }

MockK with JUnit 5 and Kotest

JUnit 5 Extension

import io.mockk.junit5.MockKExtension

@ExtendWith(MockKExtension::class)
class UserServiceTest {

    @MockK
    lateinit var repository: UserRepository

    @SpyK
    var service = UserService()

    @RelaxedMockK
    lateinit var logger: Logger

    @Test
    fun `creates user`() {
        every { repository.save(any()) } returns user
        service.createUser("Alice")
        verify { repository.save(match { it.name == "Alice" }) }
    }
}

Kotest Integration

import io.kotest.core.spec.style.FunSpec
import io.mockk.*

class UserServiceTest : FunSpec({

    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    beforeEach {
        clearMocks(repository)
    }

    test("creates user successfully") {
        every { repository.save(any()) } returns Unit
        service.createUser("Alice")
        verify { repository.save(match { it.name == "Alice" }) }
    }
})

clearMocks(mock) resets stub and interaction history between tests.

Common Patterns

Verify no other calls:

verify { repository.findById(1) }
confirmVerified(repository)  // fails if repository.save() was also called

Answer with delay (for testing timeouts):

coEvery { service.fetchData() } coAnswers {
    delay(5000)
    data
}

Sequential returns:

every { generator.next() } returnsMany listOf(1, 2, 3)
// first call → 1, second → 2, third → 3, subsequent → 3 (last value repeats)

Cleanup

Always clean up mocks to avoid test pollution:

@AfterEach
fun tearDown() {
    unmockkAll()  // clears all mocked objects, statics, and constructors
}

Or scope with mockkObject { } and mockkStatic { } blocks that clean up automatically.

Production Monitoring

MockK tests verify behavior against mocked dependencies in isolation. For confidence that the real integrations work — the external APIs, databases, and services you mocked away — HelpMeTest monitors live endpoints 24/7. No code, no mocks, just real behavior.

Summary

  • mockk<T>() creates strict mocks; mockk<T>(relaxed = true) returns defaults
  • every { } returns / answers {} for stubbing; coEvery for suspend functions
  • verify { } / coVerify for verifying calls with matchers and counts
  • slot<T>() captures arguments for detailed inspection
  • spyk() wraps real objects, delegating by default
  • mockkObject, mockkStatic for Kotlin objects and extension functions
  • clearMocks(mock) / unmockkAll() for test isolation

Read more