mockito-kotlin icon indicating copy to clipboard operation
mockito-kotlin copied to clipboard

Coroutine/suspend function type mocking support

Open MedetZhakupov opened this issue 4 years ago • 2 comments
trafficstars

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?

MedetZhakupov avatar Jan 05 '21 08:01 MedetZhakupov

Having the same issue when using IR.

jgavazzisp avatar Mar 01 '21 20:03 jgavazzisp

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. 😆

gmk57 avatar Mar 03 '23 10:03 gmk57