Search code examples
callbackkotlinmockingmockitokotlinx.coroutines

In kotlin, how do I mock a suspend function that wraps a callback?


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


Solution

  • 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