Mokkery Documentation
repository·master·Indexed 19 days ago
https://github.com/lupuuss/mokkeryA boilerplate-free mocking library for Kotlin Multiplatform (KMP) that utilizes a compiler plugin. Mokkery provides an API inspired by MockK, supporting the creation of mocks via `mock<T>`, behavior definition with `every` and `everySuspend`, and interaction verification with `verifySuspend`. It includes advanced features for coroutines through the `mokkery-coroutines` module, such as the Awaits API for synchronizing with Deferred, Channels, and delays.
What's inside Mokkery
- Mokkery is a mocking library designed specifically for Kotlin Multiplatform (KMP). It is a compiler plugin-driven library that aims to be easy to use and boilerplate-free. Its API is highly inspired by MockK, making it intuitive for developers familiar with that library.
Configure Mock modes
masterMokkery provides four modes to handle calls to functions or properties that have no defined behavior. You can specify the mode when creating a mock or set a global default.
Available Modes
strict: (Default) Throws a runtime exception if a member is called without a defined answer.autoUnit: Likestrict, but does not fail for functions returningUnit.autofill: Returns default empty values (e.g.,0for numbers,""for strings,nullfor complex types).original: Calls the super implementation if available (e.g., a default implementation in an interface). Otherwise, it fails.
Usage
Per-mock configuration:
import dev.mokkery.MockMode.strict import dev.mokkery.MockMode.autoUnit import dev.mokkery.MockMode.autofill import dev.mokkery.MockMode.original val foo = mock<Foo>(strict)Global default configuration:
mokkery { defaultMockMode.set(MockMode.autoUnit) }import dev.mokkery.MockMode mokkery { defaultMockMode.set(MockMode.autoUnit) }Supported and unsupported types for mocking
masterMokkery can mock types that are fully overridable.
Supported types:
- Interfaces
- Functional types
- Final classes (requires the all-open plugin)
- Abstract/open classes with a public constructor and all overridable public members (Note: you can allow final or inline members using specific guides).
Unsupported types:
- Functions (including extension functions)
- Final classes that are already compiled (where the
all-openplugin cannot be applied) - Primitives
- Sealed types
- Objects
Use Soft verification modes
masterBy default,
verifyusesVerifyMode.soft. This mode checks if the calls specified in the verification block occurred and marks those specific calls as verified. It does not care if other calls were made to the mock that were not part of the block.You can restrict the number of expected calls using these functions:
atLeast(n)atMost(n)exactly(n)inRange(min, max)
```kotlin mock.getAt(1) mock.getAt(2) mock.getAll() // Verifies that getAt was called at most once verify(atMost(1)) { mock.getAt(any()) // ❌ - 2 matching calls found, but expected 1 at most } ```埋Kotlin Reflect limitations with mocks
masterGenerated mocks do not work correctly with Kotlin Reflect. Accessing
.memberson a mock class will only return members fromkotlin.Any.Note: Java reflection works correctly with mocks.
val klass = mock<Foo>()::class klass.members // <- returns only members from kotlin.AnyHow MokkerySuiteScope works
masterMokkery achieves strict exhaustiveness by providing extension function overloads for core functions. When you are within a
MokkerySuiteScope(either by implementing the interface or using awithblock):mock<T>()callsfun MokkerySuiteScope.mock(...), which registers the mock into the scope.verify(...)andverifySuspend(...)call theMokkerySuiteScopeextensions, which makes the verification engine aware of all mocks registered in that specific scope.
This allows the engine to detect if a mock was interacted with but never verified.
Thread safety in Mokkery 3
masterIn Mokkery 3, mocks are fully thread-safe. You can perform configuration (e.g.,
every), verification (e.g.,verify), and method invocation from multiple threads simultaneously without manual synchronization.Important: While the mock itself is thread-safe, any custom logic you provide in a matcher or an answer (via
calls { ... }) must handle its own synchronization if it performs side effects on shared state. To avoid race conditions in your test logic, use thread-safe primitives likeatomic-fu.// Avoid this: unsynchronized side effect in an answer var x: Int = 0 every { mock.getAndIncrement() } calls { x++ } // Do this: use atomic-fu for synchronized side effects var x = atomic<Int>(0) every { mock.getAndIncrement() } calls { x.getAndIncrement() }How to extend the Answers API
masterThe Answers API allows you to define custom behavior by implementing the
Answer<T>interface and registering it viaAnsweringScope.answers.Core Concepts
Answer<T>: Represents custom behavior. It must implementcall(MokkeryBlockingCallScope)for regular functions andcall(MokkerySuspendCallScope)for suspend functions.AnsweringScope<T>: The scope where answers are registered.- Type Safety: To prevent registering a suspend-only answer on a blocking call, Mokkery uses marker interfaces:
BlockingAnsweringScope: Returned byevery.SuspendAnsweringScope: Returned byeverySuspend.
Best Practices for Custom Answers
- Use existing answers: Instead of implementing
Answerfrom scratch, usecalls { ... }inside an extension function to reuse built-in logic. - Implement
description(): Overridedescription()in yourAnswerimplementation to provide human-readable output for debugging (e.g., withprintMokkeryDebug). - Use Helper Interfaces: When implementing
Answer, use these to reduce boilerplate:Answer.Suspending: For suspend-only answers (throws exception on blocking calls).Answer.Blocking: For blocking-only answers (throws exception on suspend calls).Answer.Unified: For logic that applies to both contexts (implements a singleMokkeryCallScopeoverload).
Example: Creating a custom suspend-only answer
// 1. Define the Answer implementation private class ReturnsDelayedAnswer<T>(private val value: T) : Answer<T> { public fun call(scope: MokkeryBlockingCallScope): T = error("Not supported") public suspend fun call(scope: MokkerySuspendCallScope): T { delay(1_000) return value } override fun description() = "returnsDelayed $value" } // 2. Expose via an extension on SuspendAnsweringScope infix fun SuspendAnsweringScope<T>.returnsDelayed(value: T) { answers(ReturnsDelayedAnswer(value)) } // Usage: everySuspend { mock.fetchAt(any()) } returnsDelayed 10// Example: A suspend-only answer private class ReturnsDelayedAnswer<T>(private val value: T) : Answer<T> { public fun call(scope: MokkeryBlockingCallScope): T = error("Not supported") public suspend fun call(scope: MokkerySuspendCallScope): T { delay(1_000) return value } override fun description() = "returnsDelayed $value" } infix fun SuspendAnsweringScope<T>.returnsDelayed(value: T) { answers(ReturnsDelayedAnswer(value)) } // Usage: everySuspend { mock.fetchAt(any()) } returnsDelayed 10Restrictions on `every` and `verify` blocks
masterThe compiler plugin transforms the code inside
every { ... }andverify { ... }blocks. This imposes two main restrictions:- No extraction of parts: You cannot extract calls made inside the block into separate functions.
- Lambda requirement: The block parameter must be a lambda expression. You cannot use a function reference or a lambda assigned to a variable.
Illegal (extracting parts):
@Test fun test() { verify { extracted() } } private fun MokkeryMatcherScope.extracted() { foo.getAll() }Allowed (extracting the whole call): You can extract the entire
verifyoreverycall into a separate function.Allowed:
@Test fun test() { extracted() } private fun extracted() { verify { foo.getAll() } }// Illegal: extracting parts of the block @Test fun test() { verify { extracted() } } private fun MokkeryMatcherScope.extracted() { foo.getAll() } // Allowed: extracting the entire call @Test fun test() { extracted() } private fun extracted() { verify { foo.getAll() } }Customize spy behavior and verify calls
masterOnce a spy is created, it behaves like a mock in terms of customization:
- Changing Behavior: You can change the behavior of specific methods on the spy using the same syntax used for mocks.
- Verifying Calls: You can verify that specific methods were called on the spy instance by following the verification patterns provided in the Verifying guide.
Use the Awaits API to control coroutine execution
masterThemokkery-coroutinesmodule provides theawaitsAPI, which allows you to synchronize mock behavior with coroutine primitives likeDeferred,Channel, or specific delays. This is useful for testing complex asynchronous flows.How shared functions behave in mockMany
masterWhen using
mockMany, if multiple types share a function with the exact same signature, Mokkery treats them as a single shared function.When you define behavior for this shared function using one of the type extensions (e.g.,
t1.sharedFunction()), that behavior is applied to all types in the mock that implement that specific signature. Calling the function on any of the other type extensions (e.g.,t2.sharedFunction()) will return the same mocked value.interface A { fun sharedFunction(i: Int): String } interface B { fun sharedFunction(i: Int): String } val mock = mockMany<A, B> { every { t1.sharedFunction(any()) } returns "Hello world!" } // Both calls return the same mocked value val resultA = mock.t1.sharedFunction(1) // "Hello world!" val resultB = mock.t2.sharedFunction(1) // "Hello world!"