22

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

Mircea Nistor
  • 3,145
  • 1
  • 26
  • 32
  • It's not actually expecting a matcher for the callback, but for the implicit continuation parameter all `suspend fun`s declare at the class file level. Try providing it with an empty `Continuation`. – Marko Topolnik Aug 03 '18 at 13:31
  • `whenever(myService.doSomething(anyString(), any(Continuation.class))` – Marko Topolnik Aug 03 '18 at 14:06
  • That doesn't compile, nor does `any>()`. 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. – Mircea Nistor Aug 03 '18 at 14:37
  • 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. – Mircea Nistor Aug 03 '18 at 14:39
  • 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`. – Marko Topolnik Aug 03 '18 at 16:02
  • [Here](https://fernandocejas.com/2014/04/08/unit-testing-asynchronous-methods-with-mockito/), this post shows you the relevant Mockito syntax. It's `doAnswer(Answer).when(...)` – Marko Topolnik Aug 03 '18 at 16:08

2 Answers2

27

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
moffeltje
  • 4,521
  • 4
  • 33
  • 57
Adib Faramarzi
  • 3,798
  • 3
  • 29
  • 44
  • 1
    Mockk does not support generic at all. If I have a method that return Result it is not possible to mock it at all – Vetalll Aug 02 '22 at 08:47
19

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)
}
Vahab Ghadiri
  • 2,016
  • 20
  • 26