Android 如何从第3页测试分页数据
MyViewModel有一个返回分页数据流的方法。在我的应用程序中,数据从远程服务器获取,然后保存到房间(真相的单一来源):Android 如何从第3页测试分页数据,android,integration-testing,robolectric,kotlin-flow,android-paging-3,Android,Integration Testing,Robolectric,Kotlin Flow,Android Paging 3,MyViewModel有一个返回分页数据流的方法。在我的应用程序中,数据从远程服务器获取,然后保存到房间(真相的单一来源): 我想知道我是否在正确的轨道上。任何帮助都将不胜感激 基本上有两种方法,这取决于您想要转换前还是转换后的数据 如果您只想断言存储库端,那么您的查询是正确的-您可以直接查询PagingSource,这是预转换,尽管如此,您对ViewModel中的PagingData所做的任何映射或筛选都不会在这里被考虑。但是,如果您想直接测试查询,它更“纯粹” @Test fun repo(
我想知道我是否在正确的轨道上。任何帮助都将不胜感激 基本上有两种方法,这取决于您想要转换前还是转换后的数据 如果您只想断言存储库端,那么您的查询是正确的-您可以直接查询
PagingSource
,这是预转换,尽管如此,您对ViewModel中的PagingData所做的任何映射或筛选都不会在这里被考虑。但是,如果您想直接测试查询,它更“纯粹”
@Test
fun repo() = runBlockingTest {
val pagingSource = MyPagingSource()
val loadResult = pagingSource.load(...)
assertEquals(
expected = LoadResult.Page(...),
actual = loadResult,
)
}
另一种方法是,如果您关心转换,则需要将数据从PagingData
加载到presenter API中
@Test
fun ui() = runBlockingTest {
val viewModel = ... // Some AndroidX Test rules can help you here, but also some people choose to do it manually.
val adapter = MyAdapter(..)
// You need to launch here because submitData suspends forever while PagingData is alive
val job = launch {
viewModel.flow.collectLatest {
adapter.submitData(it)
}
}
... // Do some stuff to trigger loads
advanceUntilIdle() // Let test dispatcher resolve everything
// How to read from adapter state, there is also .peek() and .itemCount
assertEquals(..., adapter.snapshot())
// We need to cancel the launched job as coroutines.test framework checks for leaky jobs
job.cancel()
}
我发现使用会容易得多
根据您的代码,我认为您的测试用例应该如下所示:
@ExperimentalTime
@ExperimentalCoroutinesApi
@Test
fun `test if receive paged chocolate data`() = runBlockingTest {
val expected = listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
coEvery {
dao().getChocolateListData()
}.returns(
listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
)
launchTest {
viewModel.getChocolates().test(
timeout = Duration.ZERO,
validate = {
val collectedData = expectItem().collectData()
assertEquals(expected, collectedData)
expectComplete()
})
}
}
我还准备了一个基本ViewModelTest类,用于处理许多设置和拆卸任务:
abstract class BaseViewModelTest {
@get:Rule
open val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
open val testCoroutineRule = CoroutineTestRule()
@MockK
protected lateinit var owner: LifecycleOwner
private lateinit var lifecycle: LifecycleRegistry
@Before
open fun setup() {
MockKAnnotations.init(this)
lifecycle = LifecycleRegistry(owner)
every { owner.lifecycle } returns lifecycle
}
@After
fun tearDown() {
clearAllMocks()
}
protected fun initCoroutine(vm: BaseViewModel) {
vm.apply {
setViewModelScope(testCoroutineRule.testCoroutineScope)
setCoroutineContext(testCoroutineRule.testCoroutineDispatcher)
}
}
@ExperimentalCoroutinesApi
protected fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.runBlockingTest(block)
protected fun launchTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.testCoroutineScope.launch(testCoroutineRule.testCoroutineDispatcher) { block }
}
至于借用的扩展函数collectData()
(谢谢!!)
还有一个幻灯片介绍了关于PagingData,您想断言/测试什么?我已经更新了我的问题。PagingData(Paging3版本3.0.0.rc01)、单元测试(JUnit4)谢谢您的回答。在测试Dao查询时,我已经执行了PagingSource查询。我必须在存储库层再次测试它吗?同样,我不想处理存储库测试中的UI,因此我不想采用后一种方法。如果要测试
Pager.flow
的输出,包括任何转换,则需要某种方法来断言PagingData
的输出。由于整个事件流都是内部的,所以唯一的方法是将其收集到某个presenter API中。这并不一定意味着UI,但您确实需要PagingDataAdapter
或AsyncPagingDataDifference
的实例。更好的测试UTIL取代这些是一个WIP FR,用于将来的分页版本,可能会作为一个单独的模块出现。再次感谢您的回答。我已经更新了我目前的进度。但是,adapter.snapshot()似乎是空的。Dao插入操作是否可以触发加载?我的想法是它可以,因为分页库在后台使用流API。此外,我在哪里可以了解这个WIP FR?提前谢谢。很抱歉,我没有在这里看到你的答复-希望我不会太晚。您正在使用runBlockingTest
,我看不到从哪里提供chocolateListAdapter
,但您需要等待实际加载本身,并在执行断言之前让它完成。e、 例如,将TestCoroutineDispatcher
传递给fetch/main dispatcher并调用advanceUntilIdle()
testImplementation "app.cash.turbine:turbine:0.2.1"
@ExperimentalTime
@ExperimentalCoroutinesApi
@Test
fun `test if receive paged chocolate data`() = runBlockingTest {
val expected = listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
coEvery {
dao().getChocolateListData()
}.returns(
listOf(
Chocolate(name = "Dove"),
Chocolate(name = "Hershey's")
)
)
launchTest {
viewModel.getChocolates().test(
timeout = Duration.ZERO,
validate = {
val collectedData = expectItem().collectData()
assertEquals(expected, collectedData)
expectComplete()
})
}
}
abstract class BaseViewModelTest {
@get:Rule
open val instantTaskExecutorRule = InstantTaskExecutorRule()
@get:Rule
open val testCoroutineRule = CoroutineTestRule()
@MockK
protected lateinit var owner: LifecycleOwner
private lateinit var lifecycle: LifecycleRegistry
@Before
open fun setup() {
MockKAnnotations.init(this)
lifecycle = LifecycleRegistry(owner)
every { owner.lifecycle } returns lifecycle
}
@After
fun tearDown() {
clearAllMocks()
}
protected fun initCoroutine(vm: BaseViewModel) {
vm.apply {
setViewModelScope(testCoroutineRule.testCoroutineScope)
setCoroutineContext(testCoroutineRule.testCoroutineDispatcher)
}
}
@ExperimentalCoroutinesApi
protected fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.runBlockingTest(block)
protected fun launchTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineRule.testCoroutineScope.launch(testCoroutineRule.testCoroutineDispatcher) { block }
}