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

3

23

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

Overcritical answered 3/8, 2018 at 13:16 Comment(6)
It's not actually expecting a matcher for the callback, but for the implicit continuation parameter all suspend funs declare at the class file level. Try providing it with an empty Continuation.Fulk
whenever(myService.doSomething(anyString(), any(Continuation.class))Fulk
That doesn't compile, nor does any<Continuation<Long>>(). Also, if I use any() for the second param, then the .thenReturn(1234L) doesn't work since it's expecting Unit, not Long If I add a second param there, I'm basically mocking the original interface call, not the suspend function.Overcritical
I don't mind mocking the original as long as the suspend variant works and responds with what I want during tests. I just don't know how to achieve that.Overcritical
Ah yes, it slipped my mind. The extension fun is actually a public static Java method in the compiled class. You won't be able to mock it. However, there are ways to mock the original with more complexity than just thenReturn.Fulk
Here, this post shows you the relevant Mockito syntax. It's doAnswer(Answer).when(...)Fulk
I
29

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
Impend answered 1/3, 2019 at 8:16 Comment(1)
Mockk does not support generic at all. If I have a method that return Result<SomeClass> it is not possible to mock it at allKenyakenyatta
P
28

When you need to

on .... do return ..

and method is suspended, with mockito you can use this:

  • use this lib mockito-kotlin
  • Mock your object, lets name it myObject (myObject has suspend method named isFoo)

then:

 myObject.stub {
    onBlocking { isFoo() }.doReturn(true)
}
Provenance answered 19/4, 2020 at 10:53 Comment(1)
Is it possible to have doReturn execute suspend funcs before it returns?Bridget
D
0

Here is a simple solution: create a utility function and call it when you mock:

inline fun <reified T> anyNonNull(): T = Mockito.any(T::class.java)

Then you can use it in your code like this:

Mockito.`when`(myService.doSomething(anyNonNull<String>())).thenReturn(1234L) 
Dumbfound answered 27/5 at 8:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.