Answers
Essentials
To define a const answer, use returns
:
everySuspend { repository.findById(id = any()) } returns Book()
To throw an exception, use throws
:
everySuspend { repository.findById(id = any()) } throws IllegalArgumentException()
To provide more complex answer, use calls
:
- For regular functions:
every { repository.findAllByTitle(any()) } calls { (title: String) -> flowOf(Book(title = title)) }
- For suspend functions:
everySuspend { repository.findById(id = any()) } calls { (id: String) ->
delay(1_000) // suspension is allowed here!
stubBook()
}
The calls
lambda provides CallArgs instance as it
and one of two CallDefinitionScope variants:
BlockingCallDefinitionScope for every
or
SuspendCallDefinitionScope for everySuspend
.
Utilities
To return specific arg, use returnsArgAt
:
everySuspend { repository.save(book = any()) } returnsArgAt 0
repository.save(Book()) // returns Book that was passed as an argument
If you need a more complex answer for functions that return Result
, use callsCatching
:
everySuspend { repository.findByIdAsResult(id = "1") } callsCatching { (id: String) ->
if (id == "1") error("Not found!")
Book(id = id)
}
repository.findByIdAsResult("1") // returns failed Result
repository.findByIdAsResult("2") // returns successful Result
To return Result.success
from functions that return Result
, use returnsSuccess
:
everySuspend { repository.findByIdAsResult(id = "1") } returnsSuccess Book(id = "1")
repository.findByIdAsResult("1") // returns successful Result with given Book
To return Result.failure
from functions that return Result
, use returnsFailure
:
everySuspend { repository.findByIdAsResult(id = "1") } returnsFailure ElementNotFoundException(id = "1")
repository.findByIdAsResult("1") // returns failed Result with given exception
To return a value provided each time by a function, use returnsBy
:
private var book = Book(...)
// ...
everySuspend { repository.findById(any()) } returnsBy ::book
repository.findById("1") // returns Book provided by `book` property
book = book.copy(title = "New title")
repository.findById("1") // returns Book provided by `book` property with changed title
For functions returning a Result
, you can use returnsSuccessBy
or returnsFailureBy
.
To throw an exception provided each time by a function, use throwsBy
:
everySuspend { repository.findById(any()) } throwsBy ::IllegalStateException
runCatching { repository.findById("1") } // throws newly created IllegalStateException
runCatching { repository.findById("1") } // throws newly created IllegalStateException
To throw IllegalStateException
with specific message (just like kotlin.error), use throwsErrorWith
:
everySuspend { repository.findById(id = any()) } throwsErrorWith "Failed!"
Sequence of answers
You can define a sequence of answers using sequentially
:
everySuspend { repository.getById(id = any()) } sequentially {
returns(stubBook("1"))
calls { stubBook("2") }
throws(IllegalStateException())
}
repository.getById("1") // returns stubBook("1")
repository.getById("2") // returns stubBook("2")
runCatching { repository.getById("3") } // throws IllegalStateException
repository.getById("4") // fails - no more answers
At the end of sequentially
block you can repeat a sequence of answers with repeat
:
everySuspend { repository.getById(id = any()) } sequentially {
returns(stubBook("1"))
repeat { returns(stubBook("2")) }
}
repository.getById("1") // returns stubBook("1")
repository.getById("2") // returns stubBook("2")
repository.getById("3") // returns stubBook("2")
repository.getById("4") // returns stubBook("2")
You can use sequentiallyRepeat
as a shorthand if you want to define repeating sequence:
everySuspend { repository.getById(id = any()) } sequentiallyRepeat {
returns(stubBook("1"))
returns(stubBook("2"))
}
repository.getById("1") // returns stubBook("1")
repository.getById("2") // returns stubBook("2")
repository.getById("3") // returns stubBook("1")
repository.getById("4") // returns stubBook("2")
You can use sequentiallyReturns
and sequentiallyThrows
as a shorthand:
everySuspend { repository.getById(id = any()) } sequentiallyReturns listOf(stubBook("1"), stubBook("2"))
repository.getById("1") // returns stubBook("1")
repository.getById("2") // returns stubBook("2")
repository.getById("3") // fails - no more answers
You can nest sequentially
calls:
everySuspend { repository.getById(id = any()) } sequentially {
returns(stubBook("1"))
sequentially {
returns(stubBook("2"))
returns(stubBook("3"))
}
returns(stubBook("4"))
}
repository.getById("1") // returns stubBook("1")
repository.getById("2") // returns stubBook("2")
repository.getById("3") // returns stubBook("3")
repository.getById("4") // returns stubBook("4")
repository.getById("5") // fails - no more answers
Calling original implementation
The most straightforward way to call original method is to use calls
overload:
everySuspend { repository.findById(any()) } calls original
repository.findById("2") // this calls original method implementation with "2"
You can pass different arguments to original call:
everySuspend { repository.findById(any()) } calls originalWith("3")
repository.findById("2") // this calls original method implementation with "3"
If mocked type is an interface, the default implementation is called.
Resolving ambiguity
While using multiple types mocking usage of original
might not be possible,
because there are multiple super calls available for single method. Instead of original
use superOf<T>
with supertype specified.
val mock = mockMany<A, B> {
every { t1.sharedMethod(any()) } calls superOf<A>()
}
mock.t1.sharedMethod(2) // this calls method implementation from A with 2
You can pass different arguments to super call:
val mock = mockMany<A, B> {
every { t1.sharedMethod(any()) } calls superWith<A>(3)
}
mock.t1.sharedMethod(2) // this calls method implementation from A with 3
Super calls API in calls
All of those features are accessible from calls
scope:
everySuspend { repository.findById(any()) } calls {
callOriginal()
callOriginalWith("3")
callSuper(BaseRepository::class)
callSuperWith(BaseRepository::class, "3")
}
Indirect supertype calls
Under the hood, original
performs call to super method from mocked type. In Kotlin source code, it is not allowed to call super method
of indirect supertype. However, this kind of call is possible to be generated on the compiler plugin level.
It's important to note that indirect super calls for Java types (including kotlin.collections.List) and for super
methods from interfaces compiled to Java defaults are not allowed.
This restriction exists because such calls are validated in the JVM bytecode and result in runtime errors.
Indirect super calls feature has to be explicitly allowed in Gradle files:
mokkery {
allowIndirectSuperCalls.set(true)
}
Calling super methods from indirect supertypes is similar to calling original methods:
everySuspend { repository.findById(any()) } calls superOf<BaseRepository>()
repository.findById("2") // this calls super method implementation from BaseRepository with "2"
You can pass different arguments to super call:
everySuspend { repository.findById(any()) } calls superWith<BaseRepository>("3")
repository.findById("2") // this calls super method implementation from BaseRepository with "3"
Custom answer
To provide custom answer implement Answer:
object RandomIntAnswer : Answer<Int> {
override fun call(scope: FunctionScope) = Random.nextInt()
}
Answer that implements only call
works for both regular functions and suspending functions.
everySuspend { repository.countAllBooks() } answers RandomIntAnswer
You can provide convenient extension for your custom answer:
fun AnsweringScope<Int>.returnsRandomInt() = answers(RandomIntAnswer)
// ...
everySuspend { repository.countAllBooks() }.returnsRandomInt()
If your answer needs suspension, it is recommended to implement Answer.Suspending:
data class DelayedConstAnswer<T>(
val value: T,
) : Answer.Suspending<T> {
override suspend fun callSuspend(scope: FunctionScope): T {
delay(1_000)
return value
}
}
For suspending answers it's highly recommended to introduce convenience extension. It's able to indicate that given answer only supports suspending functions:
infix fun <T> SuspendAnsweringScope<T>.returnsAfterDelay(value: T) = answers(DelayedConstAnswer(value))
// ...
everySuspend { repository.countAllBooks() } returnsAfterDelay 1
If you want to restrict answer only for regular functions, use BlockingAnsweringScope<T>
.