Let's say there's an interface with a callback:
interface SomeInterface {
fun doSomething(arg: String, callback: (Exception?, Long) -> Unit)
}
which I extend into a suspend function like this:
suspend fun SomeInterface.doSomething(arg: String): Long = suspendCoroutine { cont ->
this.doSomething(arg) { err, result ->
if (err == null) {
cont.resume(result)
} else {
cont.resumeWithException(err)
}
}
}
I'd like to mock this in tests, but am failing. Ideally I'd like to use something like this:
@Test
fun checkService() {
runBlocking {
val myService = mock<SomeInterface>()
whenever(myService.doSomething(anyString())).thenReturn(1234L)
val result = myService.doSomething("")
assertEquals(result, 1234L)
}
}
The above syntax fails with a mockito exception because it's expecting a matcher for the callback.
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded:
How can I mock a suspend function like that? If a similar syntax is not possible how can I have the mock call back with the desired arguments such that the suspend variant that is used throughout my code returns the desired result during tests?
Update: It seems it's not possible when it's an extension function. Based on Marko Topolnik's comment, I gather it's because an extension is simply a static function which is out of mockito's capability.
When the suspend function is a member function, then it works as expected, with my original syntax.
Here is a gist with some demo code: https://gist.github.com/mirceanis/716bf019a47826564fa57a77065f2335
A suspending function is simply a function that can be paused and resumed at a later time. They can execute a long running operation and wait for it to complete without blocking. The syntax of a suspending function is similar to that of a regular function except for the addition of the suspend keyword.
If you notice the functions closely, they can be used to resume the coroutine with a return value or with an exception if an error had occurred while the function was suspended. This way, a function could be started, paused, and resume with the help of Continuation. We just have to use the suspend keyword.
To wait for a coroutine to finish, you can call Job. join . join is a suspending function, meaning that the coroutine calling it will be suspended until it is told to resume. At the point of suspension, the executing thread is released to any other available coroutines (that are sharing that thread or thread pool).
The Kotlin compiler transforms every suspend function to be a state machine, which optimises using callbacks every time a function needs to suspend. Knowing what the compiler does under the hood now, you can better understand why a suspend function won't return until all the work that it started has completed.
I suggest using MockK for your tests, which is more coroutine-friendly.
To mock a coroutine, you can use coEvery
and returns
like below:
val interf = mockk<SomeInterface>()
coEvery { a.doSomething(any()) } returns Outcome.OK
When you need to
on .... do return ..
and method is suspended, with mockito you can use this:
then:
myObject.stub {
onBlocking { isFoo() }.doReturn(true)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With