Mockito:无法验证由于 Continuation<T> 函数参数在后台不匹配而调用了挂起函数
Mockito: can't verify a suspend function got called because of Continuation<T> function arguments NOT MATCHING under the hood
我正在为我定义的 LocalDataSource
类 编写一些单元测试,它包装了 Room 数据库的功能 DAO
,我的代码如下所示:
Room DAO 接口
@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
}
}
单元测试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
}
}
}
我在 运行 测试时遇到此错误:
Argument(s) are different! Wanted:
personDao.insert( Person( id = ...) ),
Continuation at (my package).PersonLocalDataSourceTest$givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce.invokeSuspend(PersonLocalDataSourceTest.kt:37)
Actual invocation has different arguments:
personDao.insert(Person( id = ...),
Continuation at (my package).PersonLocalDataSourceImpl$insert.invokeSuspend(PersonLocalDataSourceImpl.kt:20)
P.S. 当我更改函数 PersonLocalDataSourceImpl::insert
的定义时测试通过,如下所示:
override suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
personDao.insert(person)
TL:DR
您可以使用 coEvery
和 coVerify
模拟结果并验证挂起函数。当您声明 testImplementation "io.mockk:mockk:"
.
时,它们将可用
在下面的示例中,我展示了如何测试 suspend 函数。
协程规则
我正在使用此自定义规则进行测试。
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:"
那么您可以使用coEvery
、coVerify
、coMatch
、coAssert
、coRun
、coAnswers
或coInvoke
模拟暂停功能。
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
(如您在我的示例中所见)。
通过结构化并发,调用者可以在任何其他调度程序上调度结果。但它不应该负责决定下游功能应该使用哪个调度程序。
我正在为我定义的 LocalDataSource
类 编写一些单元测试,它包装了 Room 数据库的功能 DAO
,我的代码如下所示:
Room DAO 接口
@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
}
}
单元测试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
}
}
}
我在 运行 测试时遇到此错误:
Argument(s) are different! Wanted:
personDao.insert( Person( id = ...) ),
Continuation at (my package).PersonLocalDataSourceTest$givenPersonLocalDataSource_WhenInsertPerson_ThenPersonDaoInsertFunctionCalledOnce.invokeSuspend(PersonLocalDataSourceTest.kt:37)
Actual invocation has different arguments:
personDao.insert(Person( id = ...),
Continuation at (my package).PersonLocalDataSourceImpl$insert.invokeSuspend(PersonLocalDataSourceImpl.kt:20)
P.S. 当我更改函数 PersonLocalDataSourceImpl::insert
的定义时测试通过,如下所示:
override suspend fun insert(dispatcher: CoroutineDispatcher, person: Person) =
personDao.insert(person)
TL:DR
您可以使用 coEvery
和 coVerify
模拟结果并验证挂起函数。当您声明 testImplementation "io.mockk:mockk:"
.
在下面的示例中,我展示了如何测试 suspend 函数。
协程规则
我正在使用此自定义规则进行测试。
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:"
那么您可以使用coEvery
、coVerify
、coMatch
、coAssert
、coRun
、coAnswers
或coInvoke
模拟暂停功能。
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
(如您在我的示例中所见)。
通过结构化并发,调用者可以在任何其他调度程序上调度结果。但它不应该负责决定下游功能应该使用哪个调度程序。