如何从 Paging 3 测试 PagingData

问题描述

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

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

解决方法

基本上有两种方法可以解决此问题,具体取决于您是需要转换前数据还是转换后数据。

如果您只想断言存储库结束,您的查询是正确的 - 您可以直接查询 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()
}
,

我发现使用 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 }

}

至于从answer from another post借来的扩展函数collectData()(谢谢@Farid!!)

以及介绍turbine

的幻灯片