【问题标题】:Mockito: can't verify a suspend function got called because of Continuation<T> function arguments NOT MATCHING under the hoodMockito:无法验证挂起函数是否被调用,因为 Continuation<T> 函数参数在后台不匹配
【发布时间】:2021-01-15 19:33:01
【问题描述】:

我正在为我定义的 LocalDataSource 类编写一些单元测试,这些类包装了 Room 数据库 DAO 的功能,我的代码如下所示:

Room DAO 接口

@Dao
interface PersonDao {

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

}

LocalDataSource 类

class PersonLocalDataSourceImpl(private val personDao: PersonDao) {

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

}

单元测试类

@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

            }
        }

}

运行测试时出现此错误:

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.当我改变函数 PersonLocalDataSourceImpl::insert 的定义时测试通过了,如下所示:

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

【问题讨论】:

    标签: android unit-testing mockito kotlin-coroutines


    【解决方案1】:

    TL:DR

    您可以使用coEverycoVerify 来模拟结果并验证挂起功能。当您声明 testImplementation "io.mockk:mockk:" 时,它们将变为可用。

    在下面的示例中,我将展示如何测试 supsend 函数。

    协程规则

    我正在使用此自定义规则进行测试。

    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)
        }
    }
    

    让我们定义一个简单的Repository 和一个Dao 接口。

    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>
    }
    

    测试协程

    mock coroutines需要添加这个依赖:

    testImplementation "io.mockk:mockk:"
    

    然后您可以使用coEverycoVerifycoMatchcoAssertcoRuncoAnswerscoInvoke 来模拟挂起函数。

    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() }
      }
    

    这样你就不需要在你的测试代码中做任何上下文切换withContext。您只需致电coroutineRule.runBlocking { ... } 并设置您对模拟的期望。然后你就可以简单地验证结果了。

    注意

    我认为你不应该从外面通过 Dispatcher。使用协程(和结构化并发),实现者(库、函数等)最清楚要在哪个 Dispatcher 上运行。当您有一个从数据库读取的函数时,该函数可以使用某个 Dispatcher,例如 Dispatchers.IO(如您在我的示例中所见)。

    使用结构化并发,调用者可以在任何其他调度器上调度结果。但它不应该负责决定应该使用哪些调度程序下游功能。

    【讨论】:

    • 感谢您的回答。我同意你的最后一点“实施者最清楚要运行哪个调度程序”,尽管正如我在问题中所说,我的问题是在对象挂起函数上使用 Mockito::verify 函数时,它的实现包含另一个挂起函数调用。也许 Mockito 不适用于 kotlin 挂起函数。
    • 我明白了。您可以使用coEverycoVerify 来模拟和验证挂起函数。我在答案中更新了测试。希望对您有所帮助。
    • 这两个函数定义在什么包里?
    • 它们在testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:" 中可用。我也更新了我的答案。
    • 我认为它们是在此处定义的:mockk.io - 我还没有在 kotlinx-coroutines-core 库中看到这些函数。
    猜你喜欢
    • 2016-10-18
    • 1970-01-01
    • 1970-01-01
    • 2012-09-23
    • 1970-01-01
    • 1970-01-01
    • 2020-09-09
    • 2019-05-07
    • 1970-01-01
    相关资源
    最近更新 更多