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 UnitArgument 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 blockMocking 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 calledAnswer 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 defaultsevery { } returns/answers {}for stubbing;coEveryfor suspend functionsverify { }/coVerifyfor verifying calls with matchers and countsslot<T>()captures arguments for detailed inspectionspyk()wraps real objects, delegating by defaultmockkObject,mockkStaticfor Kotlin objects and extension functionsclearMocks(mock)/unmockkAll()for test isolation