android - Mockito:无法验证挂起函数是否被调用,因为 Continuation<T> 函数参数在后台不匹配

标签 android unit-testing mockito kotlin-coroutines

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

Room DAO 接口(interface)

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

最佳答案

长话短说

您可以使用 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 接口(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>
}

测试协程

收件人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(如您在我的示例中所见)。

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

关于android - Mockito:无法验证挂起函数是否被调用,因为 Continuation<T> 函数参数在后台不匹配,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65742496/

相关文章:

asp.net-mvc - TDD 工作单元模式 Moq 无法实例化类错误代理

spring - 模拟 Autowired ExecutorService

android - 按下按钮时 EditText 到 TextView

android - 如何使用 LinearLayout 将 TextView 右对齐

android - 无法在设备 'emulator-5554' 上安装 myapp.apk

c# - 代码覆盖率不包括 Visual Studio 中的属性

android - 当我关闭 Activity 时,服务被终止然后重新启动

reactjs - 如何在 React 中使用 Enzyme 或 React 测试库测试 Material-UI 的响应式 UI(例如隐藏、网格、断点)

java - 为什么 Mockito 模拟在没有注释的情况下在我的测试中工作

android - 使用 Robolectric 和 Mockito 在 Android 上进行 TDD