mhdwajeeh.95
mhdwajeeh.95

Reputation: 427

Mockito: can't verify a suspend function got called because of Continuation<T> function arguments NOT MATCHING under the hood

I'm writing some unit tests for my defined LocalDataSource classes that wraps the functionality of a Room databse DAO, my code looks like this:

Room DAO Interface

@Dao
interface PersonDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(person: Person)

}

LocalDataSource Class

class PersonLocalDataSourceImpl(private val personDao: PersonDao) {

    suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
        withContext(dispatcher) {
            personDao.insert(person)     // line 20
        }

}

Unit Test Class

@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class PersonLocalDataSourceTest : BaseLocalDataSourceTest() {
    
    @Test
    fun givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce() =
        runBlockingTest {

            withContext(testCoroutineDispatcher) {

                val personDao = Mockito.mock(PersonDao::class.java)
                val personLocalDataSource = PersonLocalDataSourceImpl(personDao)
                val person = mockPerson()


                personLocalDataSource.insert(testCoroutineDispatcher, person)

                Mockito.verify(personDao).insert(person)   // line 36

            }
        }

}

I'm getting this error when running the test:

Argument(s) are different! Wanted:
personDao.insert( Person( id = ...) ),
Continuation at (my package).PersonLocalDataSourceTest$givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce$1$1.invokeSuspend(PersonLocalDataSourceTest.kt:37)

Actual invocation has different arguments:
personDao.insert(Person( id = ...),
Continuation at (my package).PersonLocalDataSourceImpl$insert$2.invokeSuspend(PersonLocalDataSourceImpl.kt:20)

P.S. the test passes when I change the definition of the function PersonLocalDataSourceImpl::insert like follows:

override suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
            personDao.insert(person)

Upvotes: 6

Views: 4849

Answers (1)

ChristianB
ChristianB

Reputation: 2690

TL:DR

You can use coEvery and coVerify to mock results and verify suspend functions. They become availbe when you declare testImplementation "io.mockk:mockk:".

In the following example I show how I do testing supsend functions.

CoroutineRule

I'm using this custom Rule for testing.

class CoroutineRule(
  val testCoroutineDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(),
    TestCoroutineScope by TestCoroutineScope(testCoroutineDispatcher) {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testCoroutineDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testCoroutineDispatcher.cleanupTestCoroutines()
    }

    /**
     * Convenience method for calling [runBlockingTest] on a provided [TestCoroutineDispatcher].
     */
    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
        testCoroutineDispatcher.runBlockingTest(block)
    }
}

Let's define a simple Repository and a Dao interface.

class Repository(
  val dao: Dao,
  private val dispatcher: Dispatcher = Dispatchers.IO) {

  suspend fun load(): String = withContext(dispatcher) { dao.load() }
}

interface Dao() {
  suspend fun load(): String 

  fun fetch(): Flow<String>
}

Test Coroutines

To mock coroutines you need to add this dependency:

testImplementation "io.mockk:mockk:"

Then you can use coEvery, coVerify, coMatch, coAssert, coRun, coAnswers or coInvoke to mock suspend functions.

import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk

class RepositoryTest {

  @get:Rule val coroutineRule = CoroutineRule()

  val dao: Dao = mockk()
 
  val classUnderTest: Respository = Repository(dao, coroutineRule.testCoroutineDispatcher)

  @Test
  fun aTest() = coroutinesRule.runBlockingTest {
    // use coEvery to mock suspend function results
    coEvery { dao.load() } returns "foo"

    // use normal every for mocking functions returning flow
    every { dao.fetch() } returns flowOf("foo")
    
    val actual = classUnderTest.load()

    // AssertJ
    Assertions.assertThat(actual).isEqual("foo")

    // use coVerify to verify calls to a suspend function
    coVerify { dao.load() }
  }

This way you do not need to do any context switch withContext in your test code. You just call coroutineRule.runBlocking { ... } and setup your expectations of your mocks. Then you can simply verify the result.

Note

I think you should not pass the Dispatcher from outside. With Coroutines (and structured concurrency) the implementor (Library, Function, etc) knows best on which Dispatcher to run. When you have a function that reads from a DataBase, that function could use a certain Dispatcher like Dispatchers.IO (as you can see in my example).

With structured concurrency, the caller can dispatch the results on any other dispatcher. But it should not be responsible to decide which dispatchers down-stream function should use.

Upvotes: 5

Related Questions