【问题标题】:How To Test PagingData From Paging 3如何从 Paging 3 测试 PagingData
【发布时间】:2021-01-20 13:56:41
【问题描述】:

我的 ViewModel 有一个返回 PagingData 流的方法。在我的应用程序中,数据是从远程服务器获取的,然后保存到 Room(单一数据源):

fun getChocolates(): Flow<PagingData<Chocolate>> {
    val pagingSourceFactory = { dao().getChocolateListData() }
    return Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            maxSize = MAX_MEMORY_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = ChocolateRemoteMediator(
                api,
                dao
        ),
        pagingSourceFactory = pagingSourceFactory
    ).flow
}

如何测试这个方法?我想测试返回的流是否包含正确的数据。

到目前为止我已经尝试过:

@InternalCoroutinesApi
@Test
fun getChocolateListReturnsCorrectData() = runBlockingTest {
    val chocolateListDao: ChocolateListDao by inject()
    val chocolatesRepository: ChocolatesRepository by inject()
    val chocolateListAdapter: ChocolateListAdapter by inject()

    // 1
    val chocolate1 = Chocolate(
        name = "Dove"
    )
    val chocolate2 = Chocolate(
        name = "Hershey's"
    )

    // 2
    // You need to launch here because submitData suspends forever while PagingData is alive
    val job = launch {
        chocolatesRepository.getChocolateListStream().collectLatest {
            chocolateListAdapter.submitData(it)
        }
    }

    // Do some stuff to trigger loads
    chocolateListDao.saveChocolate(chocolate1, chocolate2)

    // How to read from adapter state, there is also .peek() and .itemCount
    assertEquals(listOf(chocolate1, chocolate2).toMutableList(), chocolateListAdapter.snapshot())

    // We need to cancel the launched job as coroutines.test framework checks for leaky jobs
    job.cancel()
}

我想知道我是否走在正确的轨道上。任何帮助将不胜感激!

【问题讨论】:

标签: android integration-testing robolectric kotlin-flow android-paging-3


【解决方案1】:

基本上有两种方法,具体取决于您需要转换前还是转换后的数据。

如果您只想断言存储库结束,您的查询是正确的 - 您可以直接查询 PagingSource,尽管这是预转换,因此您对 ViewModel 中的 PagingData 执行的任何映射或过滤都不会在这里算算。但是,如果您想直接测试查询,它会更“纯粹”。

@Test
fun repo() = runBlockingTest {
  val pagingSource = MyPagingSource()
  val loadResult = pagingSource.load(...)
  assertEquals(
    expected = LoadResult.Page(...),
    actual = loadResult,
  )
}

如果您关心转换,则需要将数据从 PagingData 加载到演示者 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()
}

【讨论】:

  • 感谢您的回答。我在测试 Dao 查询时已经做了 PagingSource 查询。我是否必须在存储库层再次对其进行测试?同样,我不想在存储库测试中处理 UI,所以我不认为我会采用后一种方法。
  • 如果你想测试Pager.flow的输出包括任何转换,你需要一些方法来断言PagingData的输出。由于整个事件流是内部的,因此唯一的方法是将其收集到一些演示者 API 中。这并不一定意味着 UI,但您确实需要 PagingDataAdapterAsyncPagingDataDiffer 的实例。替代这些的更好的 test-utils 是一个 WIP FR,用于将来的分页发布,可能会作为一个单独的模块出现。
  • 再次感谢您的回答。我已经更新了我目前的进度。但是,似乎 adapter.snapshot() 是空的。道插入操作真的可以触发负载吗?我的想法是它可以作为分页库使用引擎盖下的 Flow API。另外,我在哪里可以了解这个WIP FR?提前致谢。
  • 对不起,我没有在这里看到你的回复——希望我不会太晚。您正在使用runBlockingTest,我看不到chocolateListAdapter 是从哪里提供的,但是您需要等待实际负载本身并让它在进行断言之前完成。例如,将 TestCoroutineDispatcher 传递给 fetch / main dispatcher 并调用 advanceUntilIdle()
  • @dlam - 如何验证存储库层中的转换?我需要使用PagingDataAdapter 吗?如何在第二种方法中触发加载?你的意思是adapter.refresh?如果是,那么后续加载如何?
【解决方案2】:

我发现使用Turbine from cashapp 会容易得多。(JakeWharton 又来救援了:P)

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

我还准备了一个基本的 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()借用answer from another post的扩展功能(感谢@Farid!!)

还有介绍turbine的幻灯片

【讨论】:

    猜你喜欢
    • 2021-03-14
    • 1970-01-01
    • 2022-01-07
    • 2020-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-18
    • 1970-01-01
    相关资源
    最近更新 更多