Mocking
Let's assume we have given interface:
interface BooksRepository {
suspend fun findById(id: String): Book
suspend fun countAll(): Int
fun findAll(): Flow<Book>
}
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
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 from main source set using all-open plugin.
Apply the all-open
plugin:
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:
allOpen {
annotation("your.package.OpenForMokkery")
}
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:
// 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 { /* ... */ }
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.