Skip to main content

Mocking

warning

Please read limitations section!

Mock tracks all functions and properties calls and allows defining their answers.

To create a mock of given type, use mock function:

val foo = mock<Foo>()

If you call a member function that has no defined behavior, 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 foo = mock<Foo>(strict)

AutoUnit

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

import dev.mokkery.MockMode.autoUnit

val foo = mock<Foo>(autoUnit)

Autofill

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

import dev.mokkery.MockMode.autofill

val foo = mock<Foo>(autofill)

Original

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

import dev.mokkery.MockMode.original

val foo = mock<Foo>(original)

Default mock mode

It is possible to change the default MockMode using defaultMockMode option:

import dev.mokkery.MockMode

mokkery {
defaultMockMode.set(MockMode.autoUnit)
}

Defining answers

To define an answer for regular member function, use every:

every { foo.getAll() } returns listOf(1, 2, 3)

For property, call getter or setter inside every:

// getter
every { foo.values } returns listOf(1, 2, 3)

// setter
every { foo.values = any() } returns Unit

For suspending member function, use everySuspend:

everySuspend { foo.fetchAll() } returns listOf(1, 2, 3)

every and everySuspend can be used with function references as a shorthand:

every(foo::getAll) returns listOf(1, 2)
every(foo::values::get) returns listOf(2, 3)
every(foo::values::set) returns Unit
everySuspend(foo::fetchAt) returns 10

foo.getAll() // returns listOf(1, 2)
foo.values // returns listOf(2, 3)
foo.values = emptyList() // returns Unit
foo.fetchAt(0) // returns 10
foo.fetchAt(1) // returns 10
foo.fetchAt(2) // returns 10
danger

Make sure that you are calling a member function of a 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 behavior config to mock block:

val mock = mock<Foo>(autoUnit) {
every { getAll() } returns listOf(1, 2, 3)
everySuspend { fetchAll() } returns listOf(1, 2, 3)
}

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

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

every { foo.getAt(0) } returns 1
every { foo.getAt(1) } returns 2

foo.getAt(0) // returns 1
foo.getAt(1) // returns 2
foo.getAt(2) // error - answer not defined

To accept more broad range of parameter values, use matchers:

every { foo.getAt(any()) } returns 0

foo.getAt(0) // returns 0
foo.getAt(1) // returns 0
foo.getAt(2) // returns 0

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
every { foo.getAt(0) } returns 1
every { foo.getAt(any()) } returns 2

foo.getAt(0) // returns 2

For member function with extension parameter, you need to use special ext function:

import dev.mokkery.templating.ext

// ...

every { foo.ext { any<String>().toNumber() } } returns 10

foo.run { "10".toNumber() } // returns 10

For member function with context parameters, you need to use special ctx function:

import dev.mokkery.templating.ctx

// ...

every { ctx(any<FooContext>()) { foo.getAllWithContext() } } returns listOf(1, 2, 3)

context(FooContext()) {
foo.getAllWithContext() // returns listOf(1, 2, 3)
}
danger

ext and ctx behave like standard Kotlin scope functions, but they must be used exclusively!

Resetting answers

To reset all defined answers use resetAnswers:

every { foo.getAt(0) } returns 1

resetAnswers(mock)

foo.getAt(0) // 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 options:

build.gradle.kts
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 behavior or track them. It's possible only for overridable member functions.

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 from main source set using all-open plugin.

Apply the all-open plugin:

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

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.

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 { /* ... */ }

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.

Annotations copying

By default, Mokkery copies all the annotations from the type to mock to the implementation. This behavior might be wanted, but also it could be problematic e.g. for some annotation based frameworks. You can change the default behavior using annotations.copyToMock option:

build.gradle.kts
import dev.mokkery.options.AnnotationSelector.Companion.all
import dev.mokkery.options.AnnotationSelector.Companion.none
import dev.mokkery.options.AnnotationSelector.Companion.named
import dev.mokkery.options.AnnotationSelector.Companion.matches

mokkery {
annotations {
// No annotations
copyToMock = none

// All annotations except "example.A"
copyToMock = all - named("example.A")

// Only "example.A"
copyToMock = named("example.A")

// All annotations matching the regex "internal.*"
copyToMock = matches(Regex("internal.*"))

// Combine rules: all except "example.A" and all annotations starting with "internal"
copyToMock = all - named("example.A") - matches(Regex("internal.*"))
}
}