Mocking #

Mocking is the technique of replacing real dependencies — databases, external APIs, email services, file systems — with fake objects whose behavior you fully control inside a test. Without mocking, unit tests become integration tests: slow, dependent on external infrastructure, and non-deterministic in their results. With mocking, you can test every business logic in isolation and fast — without failing database connections or API timeouts. Kotlin has two mocking framework choices: MockK (designed specifically for Kotlin, natively supporting coroutines, extension functions, and final classes) and Mockito (the Java standard that can also be used in Kotlin). This article focuses on MockK as the primary choice, with a Mockito comparison where relevant.

Why MockK, Not Mockito? #

Mockito was designed for Java — it has fundamental limitations when dealing with Kotlin features:

// Problems with Mockito in Kotlin:
// 1. All Kotlin classes are final by default — Mockito can't mock final classes
//    without the extra mockito-inline configuration
// 2. Extension functions can't be mocked with Mockito
// 3. Coroutines and suspend functions need special workarounds
// 4. data classes and companion objects are hard to mock

// MockK doesn't have these problems:
// ✓ Mocks final classes without extra configuration
// ✓ Mocks extension functions
// ✓ Mocks suspend functions natively
// ✓ Mocks objects, companion objects, and top-level functions
AspectMockKMockito
Final classes✓ NativeNeeds mockito-inline
Suspend functions✓ NativeNeeds workarounds
Extension functions✓ Can✗ Can’t
Object/companion✓ Can✗ Can’t
SyntaxIdiomatic KotlinJava-style
Coroutine support✓ CompleteLimited

Setup #

// build.gradle.kts
dependencies {
    testImplementation("io.mockk:mockk:1.13.10")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
}

Creating Mocks #

There are several ways to create mocks in MockK depending on your needs:

import io.mockk.*

interface Repository {
    fun findById(id: Int): String?
    fun save(data: String): Boolean
    fun findAll(): List<String>
}

// mockk<T>() — standard mock, all methods must be stubbed or error
val repo = mockk<Repository>()

// relaxed mock — unstubbed methods return default values
// (0, false, "", null, emptyList, etc.) without errors
val relaxedRepo = mockk<Repository>(relaxed = true)

// relaxUnitFun — Unit-returning methods don't need stubbing
val repoRelaxUnit = mockk<Repository>(relaxUnitFun = true)

// spyk<T>() — spy: calls the original implementation but can be partially overridden
class EmailService {
    fun send(to: String, subject: String) = println("Send to $to: $subject")
    fun format(message: String) = message.uppercase()
}

val serviceSpy = spyk(EmailService())
// serviceSpy.send() calls the original implementation
// but we can override format() for the test

Stubbing with every #

every { } defines a mock’s behavior when a specific method is called:

val repo = mockk<Repository>()

// Return a specific value
every { repo.findById(1) } returns "Product A"
every { repo.findById(2) } returns null
every { repo.save(any()) } returns true
every { repo.findAll() } returns listOf("A", "B", "C")

// Return different values on successive calls
every { repo.findById(1) } returnsMany listOf("First", "Second", "Third")
// Call 1: "First"
// Call 2: "Second"
// Call 3+: "Third" (the last value repeats)

// Throw an exception
every { repo.findById(-1) } throws IllegalArgumentException("Invalid ID")

// Run a lambda when the method is called
every { repo.save(any()) } answers { call ->
    val data = call.invocation.args[0] as String
    println("Mock saving: $data")
    data.isNotBlank()
}

// answers with firstArg for accessing the first argument
every { repo.findById(any()) } answers { "Data-${firstArg<Int>()}" }

Argument Matchers #

Matchers enable more flexible stubbing:

// any() — matches any argument of that type
every { repo.findById(any()) } returns "Data"

// specific value — matches only a specific value
every { repo.findById(42) } returns "Special Data"

// matching { } — custom conditions
every { repo.findById(matching { it > 0 }) } returns "Valid ID"
every { repo.findById(matching { it <= 0 }) } throws IllegalArgumentException()

// capture — capture arguments to inspect later
val idSlot = slot<Int>()
every { repo.findById(capture(idSlot)) } returns "Captured"

repo.findById(99)
println(idSlot.captured)  // 99

// captureNullable — for nullable arguments
val nullableSlot = slot<String?>()

// Matchers for special types
every { repo.findById(ofType(Int::class)) } returns "Int"
every { repo.findById(isNull()) } returns null
every { repo.findById(isNull(inverse = true)) } returns "Non-null"

Interaction Verification with verify #

verify { } ensures that a specific method was called with the correct arguments:

val repo = mockk<Repository>(relaxed = true)
val service = ProductService(repo)

// Run the code under test
service.findProduct(5)

// Basic verification — the method was called once
verify { repo.findById(5) }

// Verify the call count
verify(exactly = 1) { repo.findById(5) }
verify(exactly = 0) { repo.save(any()) }  // never called
verify(atLeast = 1) { repo.findById(any()) }
verify(atMost = 3)  { repo.findById(any()) }

// Verify it wasn't called
verify(exactly = 0) { repo.delete(any()) }
// Or more expressively:
confirmVerified(repo)  // make sure there are no unverified calls

// Verify the call order
val repo2 = mockk<Repository>(relaxed = true)
repo2.findAll()
repo2.findById(1)
repo2.save("data")

verifyOrder {
    repo2.findAll()
    repo2.findById(1)
    repo2.save("data")
}

// Verify a strict sequence (no calls in between allowed)
verifySequence {
    repo2.findAll()
    repo2.findById(1)
    repo2.save("data")
}

Complete Scenario: Testing a Service Layer #

This is the most common mocking use — testing business logic without touching a real database or API:

// Production code under test
interface UserRepository {
    fun findById(id: Int): User?
    fun save(user: User): User
    fun delete(id: Int): Boolean
    fun emailExists(email: String): Boolean
}

interface EmailService {
    fun sendWelcome(email: String, name: String)
}

data class User(val id: Int, val name: String, val email: String, val active: Boolean = true)

class UserService(
    private val repo: UserRepository,
    private val emailService: EmailService
) {
    fun registerUser(name: String, email: String): User {
        require(name.isNotBlank()) { "Name must not be empty" }
        require(email.contains("@")) { "Invalid email format" }

        if (repo.emailExists(email)) {
            throw IllegalStateException("Email $email is already registered")
        }

        val newUser = User(id = 0, name = name, email = email)
        val saved = repo.save(newUser)
        emailService.sendWelcome(saved.email, saved.name)

        return saved
    }

    fun deactivateUser(id: Int): Boolean {
        val user = repo.findById(id)
            ?: throw NoSuchElementException("User $id not found")

        if (!user.active) return false

        val updated = user.copy(active = false)
        repo.save(updated)
        return true
    }
}

// Test
class UserServiceTest {

    private val repo = mockk<UserRepository>()
    private val emailService = mockk<EmailService>(relaxUnitFun = true)
    private val service = UserService(repo, emailService)

    @Test
    fun `registering a new user succeeds and sends a welcome email`() {
        // Arrange
        val name = "Budi Santoso"
        val email = "[email protected]"
        val savedUser = User(id = 1, name = name, email = email)

        every { repo.emailExists(email) } returns false
        every { repo.save(any()) } returns savedUser

        // Act
        val result = service.registerUser(name, email)

        // Assert
        assertEquals(1, result.id)
        assertEquals(name, result.name)
        assertEquals(email, result.email)

        // Verify interactions
        verify { repo.emailExists(email) }
        verify { repo.save(match { it.name == name && it.email == email }) }
        verify { emailService.sendWelcome(email, name) }
    }

    @Test
    fun `registering a user with an existing email throws an exception`() {
        // Arrange
        every { repo.emailExists("[email protected]") } returns true

        // Act & Assert
        val exception = assertThrows<IllegalStateException> {
            service.registerUser("Name", "[email protected]")
        }
        assertTrue(exception.message!!.contains("already registered"))

        // Verify: repo.save() must not be called
        verify(exactly = 0) { repo.save(any()) }
        verify(exactly = 0) { emailService.sendWelcome(any(), any()) }
    }

    @Test
    fun `deactivating an active user succeeds`() {
        val activeUser = User(1, "Budi", "[email protected]", active = true)

        every { repo.findById(1) } returns activeUser
        every { repo.save(any()) } returns activeUser.copy(active = false)

        val result = service.deactivateUser(1)

        assertTrue(result)
        verify { repo.save(match { !it.active }) }
    }

    @Test
    fun `deactivating an already inactive user returns false`() {
        val inactiveUser = User(2, "Sari", "[email protected]", active = false)

        every { repo.findById(2) } returns inactiveUser

        val result = service.deactivateUser(2)

        assertFalse(result)
        verify(exactly = 0) { repo.save(any()) }
    }

    @Test
    fun `deactivating a non-existent user throws NoSuchElementException`() {
        every { repo.findById(999) } returns null

        assertThrows<NoSuchElementException> {
            service.deactivateUser(999)
        }
    }
}

Mocking Suspend Functions and Coroutines #

MockK supports suspend functions natively — no workarounds needed:

interface UserApi {
    suspend fun fetchUser(id: Int): User
    suspend fun saveUser(user: User): Boolean
}

class AsyncUserService(private val api: UserApi) {
    suspend fun update(id: Int, newName: String): User {
        val user = api.fetchUser(id)
        val updated = user.copy(name = newName)
        api.saveUser(updated)
        return updated
    }
}

class SuspendFunctionTest {

    @Test
    fun `updating a user's name succeeds`() = runTest {
        val api = mockk<UserApi>()
        val service = AsyncUserService(api)

        val oldUser = User(1, "Old Name", "[email protected]")
        val newName = "New Name"

        // coEvery — for suspend functions (co = coroutine)
        coEvery { api.fetchUser(1) } returns oldUser
        coEvery { api.saveUser(any()) } returns true

        val result = service.update(1, newName)

        assertEquals(newName, result.name)

        // coVerify — verify suspend functions
        coVerify { api.fetchUser(1) }
        coVerify { api.saveUser(match { it.name == newName }) }
    }

    @Test
    fun `fetching a user fails and throws an exception`() = runTest {
        val api = mockk<UserApi>()
        val service = AsyncUserService(api)

        coEvery { api.fetchUser(999) } throws RuntimeException("User not found")

        assertThrows<RuntimeException> {
            runBlocking { service.update(999, "Name") }
        }
    }
}

Mocking Objects and Companion Objects #

MockK can mock singleton objects and companion objects — something Mockito can’t do:

object AppConfig {
    fun getVersion(): String = "1.0.0"
    fun isDebug(): Boolean = false
}

class VersionService {
    fun versionInfo() = "App v${AppConfig.getVersion()} (debug=${AppConfig.isDebug()})"
}

class ObjectMockTest {

    @Test
    fun `mock a singleton object`() {
        // mockkObject — use to mock a singleton object
        mockkObject(AppConfig)

        every { AppConfig.getVersion() } returns "2.0.0-test"
        every { AppConfig.isDebug() } returns true

        val service = VersionService()
        val info = service.versionInfo()

        assertEquals("App v2.0.0-test (debug=true)", info)

        // Important: unmock after finishing so it doesn't affect other tests
        unmockkObject(AppConfig)
    }
}

Mocking Top-Level Functions #

// In Utils.kt
fun calculateTax(price: Double): Double = price * 0.11

// Test
class TopLevelFunctionTest {

    @Test
    fun `mock a top-level function`() {
        mockkStatic(::calculateTax)

        every { calculateTax(any()) } returns 0.0  // no tax in tests

        val result = calculateTax(100_000.0)
        assertEquals(0.0, result)

        unmockkStatic(::calculateTax)
    }
}

The @MockK Annotation — More Concise #

For test classes with many mocks, annotations are more concise than manual initialization:

import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)  // enable annotations in JUnit 5
class UserServiceAnnotationTest {

    @MockK
    lateinit var repo: UserRepository

    @MockK
    lateinit var emailService: EmailService

    @RelaxedMockK  // equivalent to mockk(relaxed = true)
    lateinit var logger: Logger

    @SpyK
    var calculatorSpy = Calculator()

    private lateinit var service: UserService

    @BeforeEach
    fun setUp() {
        service = UserService(repo, emailService)
    }

    @Test
    fun `test with annotation mocks`() {
        every { repo.emailExists(any()) } returns false
        every { repo.save(any()) } returns User(1, "Test", "[email protected]")
        justRun { emailService.sendWelcome(any(), any()) }

        val result = service.registerUser("Test", "[email protected]")
        assertEquals(1, result.id)
    }
}

When Not to Use Mocks #

Excessive mocking is an anti-pattern that makes tests fragile and hard to understand. A guide for when mocks are appropriate and when they aren’t:

USE mocks if:
  ✓ The dependency involves I/O: databases, APIs, files, email
  ✓ The dependency is slow or non-deterministic
  ✓ The dependency has side effects (sending email, debiting an account)
  ✓ You want to test error scenarios that are hard to reproduce
    (database timeouts, network drops, full disks)

DON'T mock if:
  ✗ A simple class without external dependencies — use the real instance
  ✗ Data classes, value objects — use real ones
  ✗ The class under test itself — use the real instance
  ✗ Frameworks/libraries you don't control — use fakes or in-memory versions
  ✗ Too many mocks make the test not reflect real behavior
// ANTI-PATTERN: mocking a data class unnecessarily
val mockUser = mockk<User>()
every { mockUser.name } returns "Budi"
every { mockUser.email } returns "[email protected]"

// CORRECT: use it directly
val user = User(id = 1, name = "Budi", email = "[email protected]")

// ANTI-PATTERN: mocking simple calculations
val mockCalculator = mockk<Calculator>()
every { mockCalculator.add(2, 3) } returns 5

// CORRECT: use the real instance
val calculator = Calculator()
assertEquals(5, calculator.add(2, 3))

Summary #

  • MockK for Kotlin, not Mockito — MockK is designed for Kotlin: it can mock final classes without configuration, suspend functions natively, extension functions, and singleton objects. Mockito has fundamental limitations in Kotlin.
  • mockk<T>() vs mockk<T>(relaxed = true) — the standard mock errors when a method isn’t stubbed (good for finding unexpected calls). Relaxed mocks fit dependencies where many methods are irrelevant to this test.
  • coEvery and coVerify for suspend functions — use the co- variants for all interactions with suspend functions. The syntax is identical to regular every/verify.
  • Capture arguments with slot<T>() — capture arguments passed to a mock to inspect their values after the call. Useful for verifying data sent to a repository.
  • verifyOrder and verifySequence for ordering — when the call order matters (e.g., “fetch data first, then save”), use verifyOrder or verifySequence.
  • mockkObject for singletons — mock Kotlin objects or top-level functions with mockkObject and mockkStatic. Always unmockkObject after a test so it doesn’t affect other tests.
  • @MockK annotation with @ExtendWith(MockKExtension::class) — more concise than manual initialization. All mocks are injected automatically before every test.
  • Don’t mock data classes or simple classes — use real instances. Mock only dependencies with I/O, side effects, or non-determinism.

← Previous: Unit Testing   Next: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact