Skip to main content

Mocking

Let's assume we have given interface:

interface BooksRepository {

suspend fun findById(id: String): Book

suspend fun countAll(): Int

fun findAll(): Flow<Book>
}
warning

Please read limitations section!

Mock tracks all method calls and allows defining their answers.

To create a mock of BooksRepository use mock function:

val repository = mock<BooksRepository>()

If you call a method that has no defined behaviour, runtime exception is thrown. It's dictated by MockMode.strict.

Mock modes

Mokkery provides 4 modes for missing answers.

Strict

It is the default mode that fails on missing answers.

import dev.mokkery.MockMode.strict

val repository = mock<BookRepository>(strict)

AutoUnit

Just like strict but it does not fail on Unit returning methods.

import dev.mokkery.MockMode.autoUnit

val repository = mock<BookRepository>(autoUnit)

Autofill

Returns empty values e.g. 0 for numbers, "" for string and null for complex types.

import dev.mokkery.MockMode.autofill

val repository = mock<BookRepository>(autofill)

Original

Calls super implementation if available (default implementation for interface). Otherwise, it fails. Useful for mocking types highly dependant on default behaviour.

import dev.mokkery.MockMode.original

val repository = mock<BookRepository>(original)

Default mock mode

It is possible to change the default MockMode on the Gradle plugin level like this:

import dev.mokkery.MockMode

mokkery {
defaultMockMode.set(MockMode.autoUnit)
}

Defining answers

To define an answer for regular function use every:

every { repository.findAll() } returns flowOf(Book(...))

For suspending function use everySuspend:

everySuspend { repository.countAll() } returns 1
danger

Make sure that you are calling a method of mock type! Mocking extension functions is not supported!

It's worth to notice that everySuspend is not suspending function, so it's possible to configure suspending functions in non-suspending context e.g. test class property.

You can move your behaviour config to mock block:

val repository = mock<BookRepository>(autoUnit) {
every { findAll() } returns flowOf(Book(...))
everySuspend { countAll() } returns 1
}

returns is quite simple and returns always the same value. If you want to discover other behaviours check answers guide!

If a method accepts parameters, you can define an answer only for specific parameters:

everySuspend { repository.findById("1") } returns Book(id = "1", ...)
everySuspend { repository.findById("2") } returns Book(id = "1", ...)

repository.findById("1") // returns Book(id = "1", ...)
repository.findById("2") // returns Book(id = "2", ...)
repository.findById("3") // error - answer not defined

To accept more broad range of parameter values use matchers:

everySuspend { repository.findById(any()) } returns Book(id = "1", ...)

repository.findById("1") // returns Book(id = "1", ...)
repository.findById("2") // returns Book(id = "1", ...)
repository.findById("3") // returns Book(id = "1", ...)

In case of a call that matches more than one answer, the later defined takes precedence:

// this answer is unreachable as the later defined matches all possible calls
everySuspend { repository.findById("1") } returns Book(id = "1", ...)
everySuspend { repository.findById(any()) } returns Book(id = "2", ...)

repository.findById("1") // returns Book(id = "2", ...)

Resetting answers

To reset all defined answers use resetAnswers:

everySuspend { repository.findById("1") } returns Book(id = "1", ...)

resetAnswers(repository)

repository.findById("1") // error - answer not defined

Abstract/open class with final members

By default, it is illegal to mock open or abstract types with final (inline included) members.

You can ignore those members with following Gradle options:

mokkery {
ignoreInlineMembers.set(true) // ignores only inline members
ignoreFinalMembers.set(true) // ignores final members (inline included)
}

With given flags, Mokkery ignores illegal members, but it's still not possible to change their behaviour or track them. It's possible only for overridable methods.

Final classes

Mocking final classes that are already compiled is currently not possible. This includes any class defined in the main source set. However, you can "open" your final classes in the main source set using the all-open plugin.

Mocking final classes that are already compiled is currently not possible. This includes any class defined in the main source set. However, you can "open" your final classes from main source set using all-open plugin.

First, apply the all-open plugin:

build.gradle.kts
plugins {
// ...
kotlin("plugin.allopen")
}

Second, choose one of two strategies:

Open specific classes

Define an annotation:

package your.package

annotation class OpenForMokkery()

Add OpenForMokkery to final classes that you want to mock:

@OpenForMokkery
class Foo {

fun foo() = Unit
}

Configure all-open plugin:

build.gradle.kts
allOpen {
annotation("your.package.OpenForMokkery")
}
danger

Specified classes are now open in production. This should be fine for apps but can be problematic if your artifacts are consumed by other libraries. Refer to this section for further guidance.

Open all classes

All Kotlin classes are annotated with kotlin.Metadata annotation, so you can use it with all-open plugin:

build.gradle.kts
allOpen {
annotation("kotlin.Metadata")
}
danger

All classes in your project are now open in production. This should be fine for apps but can be problematic if your artifacts are consumed by other libraries. Refer to this section for further guidance.

Avoid opening production code

To avoid opening classes in production, you can conditionally apply the allOpen configuration based on whether a testing task is being executed:

build.gradle.kts
// this check might require adjustment depending on your project type and the tasks that you use
// `endsWith("Test")` works with "*Test" tasks from Multiplafrom projects, but it does not include tasks like `check`
fun isTestingTask(name: String) = name.endsWith("Test")

val isTesting = gradle
.startParameter
.taskNames
.any(::isTestingTask)

if (isTesting) allOpen { /* selected config here */ }

danger

gradle.startParameter.taskNames only includes explicitly selected tasks. If you run a task that depends on a test task but does not match isTestingTask, the all-open plugin will not be configured. Adjust the task name condition based on your project type and the tasks you use.