Mokkery Documentation

repository·master·Indexed 19 days ago

https://github.com/lupuuss/mokkery

A 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.

Tokens
15.6K
Snippets
64
Records
73
Agent score
62%

What's inside Mokkery

  1. Overview of Mokkery

    master
    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.
  2. Configure Mock modes

    master

    Mokkery 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: Like strict, but does not fail for functions returning Unit.
    • autofill: Returns default empty values (e.g., 0 for numbers, "" for strings, null for 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)
    }
  3. Supported and unsupported types for mocking

    master

    Mokkery 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-open plugin cannot be applied)
    • Primitives
    • Sealed types
    • Objects
  4. Use Soft verification modes

    master

    By default, verify uses VerifyMode.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
    }
    ```埋
  5. Kotlin Reflect limitations with mocks

    master

    Generated mocks do not work correctly with Kotlin Reflect. Accessing .members on a mock class will only return members from kotlin.Any.

    Note: Java reflection works correctly with mocks.

    val klass = mock<Foo>()::class
    klass.members // <- returns only members from kotlin.Any
  6. How MokkerySuiteScope works

    master

    Mokkery achieves strict exhaustiveness by providing extension function overloads for core functions. When you are within a MokkerySuiteScope (either by implementing the interface or using a with block):

    1. mock<T>() calls fun MokkerySuiteScope.mock(...), which registers the mock into the scope.
    2. verify(...) and verifySuspend(...) call the MokkerySuiteScope extensions, 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.

  7. Thread safety in Mokkery 3

    master

    In 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 like atomic-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() }
  8. How to extend the Answers API

    master

    The Answers API allows you to define custom behavior by implementing the Answer<T> interface and registering it via AnsweringScope.answers.

    Core Concepts

    • Answer<T>: Represents custom behavior. It must implement call(MokkeryBlockingCallScope) for regular functions and call(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 by every.
      • SuspendAnsweringScope: Returned by everySuspend.

    Best Practices for Custom Answers

    1. Use existing answers: Instead of implementing Answer from scratch, use calls { ... } inside an extension function to reuse built-in logic.
    2. Implement description(): Override description() in your Answer implementation to provide human-readable output for debugging (e.g., with printMokkeryDebug).
    3. 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 single MokkeryCallScope overload).

    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 10
  9. Restrictions on `every` and `verify` blocks

    master

    The compiler plugin transforms the code inside every { ... } and verify { ... } blocks. This imposes two main restrictions:

    1. No extraction of parts: You cannot extract calls made inside the block into separate functions.
    2. 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 verify or every call 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()
       }
    }
  10. Customize spy behavior and verify calls

    master

    Once a spy is created, it behaves like a mock in terms of customization:

    1. Changing Behavior: You can change the behavior of specific methods on the spy using the same syntax used for mocks.
    2. Verifying Calls: You can verify that specific methods were called on the spy instance by following the verification patterns provided in the Verifying guide.
  11. How shared functions behave in mockMany

    master

    When 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!"