mockito / mockito-kotlin

Using Mockito with Kotlin
MIT License
3.1k stars 200 forks source link

Coroutine/suspend function type mocking support #398

Open MedetZhakupov opened 3 years ago

MedetZhakupov commented 3 years ago

Mocking below suspend function type

someFunction: suspend () -> Boolean

throws NullPointerException. If I remove suspend keyword and use simple mocking like

on { invoke() }.doReturn { true }

it also throws NullPointerException but when I change it to

onGeneric { invoke() }.doReturn { true }

then it works fine. In suspend case I am not sure what might be the alternative for that. Anyone has any clue how I can approach this?

jgavazzisp commented 3 years ago

Having the same issue when using IR.

gmk57 commented 1 year ago

I suspect this is because suspend functions have additional Continuation in the bytecode, and as a result what you stub may not match what you invoke. Might be the same issue as #247 and #379.

In simplest cases it can be worked around by using runBlocking/runTest + whenever:

private val someFunction: suspend () -> Boolean = mock()

@Test
fun `this will fail`() = runBlocking {
    someFunction.stub {
        onBlocking { invoke() } doReturn true
    }
    assertTrue(someFunction())
}

@Test
fun `this will pass`() = runBlocking {
    whenever(someFunction.invoke()) doReturn true
    assertTrue(someFunction())
}

private val fancyFunction: suspend (String) -> Int = mock()

@Test
fun `this will pass too`() = runBlocking {
    whenever(fancyFunction.invoke("a")) doReturn 42
    assertEquals(42, fancyFunction("a"))
}

The problem is, we still can't use matchers, it fails with Invalid use of argument matchers! 2 matchers expected, 1 recorded

@Test
fun `with matchers it still fails`() = runBlocking {
    whenever(fancyFunction.invoke(any())) doReturn 42
    assertEquals(42, fancyFunction("a"))
}

The only workaround I found is to use explicit interfaces, as suggested before:

interface SuspendingInterface {
    suspend fun invoke(str: String): Int
}
private val suspendingInterface: SuspendingInterface = mock()

@Test
fun `with interface it works`() = runBlocking {
    whenever(suspendingInterface.invoke(any())) doReturn 42
    assertEquals(42, suspendingInterface.invoke("a"))
}

@Test
fun `interface even works with onBlocking`() = runBlocking {
    suspendingInterface.stub {
        onBlocking { invoke(any()) } doReturn 42
    }
    assertEquals(42, suspendingInterface.invoke("a"))
}

Or to switch to MockK which doesn't have these issues. 😆