3

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

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

4

2 回答 2

1

我发现使用Cashapp 中的 Turbine会容易得多。(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()中借来的扩展功能(感谢@Farid !!)

以及介绍涡轮机的幻灯片

于 2021-05-15T16:08:25.407 回答
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()
}
于 2021-01-27T01:08:01.963 回答