【问题标题】:Unit test the new Kotlin coroutine StateFlow单元测试新的 Kotlin 协程 StateFlow
【发布时间】:2020-09-18 11:41:29
【问题描述】:

最近是 introducedStateFlow 作为 Kotlin 协程的一部分。

我目前正在尝试它并在尝试对我的 ViewModel 进行单元测试时遇到问题。我想要实现的目标:测试我的StateFlow 在我的 ViewModel 中以正确的顺序接收所有状态值。

我的代码如下:

视图模型:

class WalletViewModel(private val getUserWallets: GetUersWallets) : ViewModel() {

val userWallet: StateFlow<State<UserWallets>> get() = _userWallets
private val _userWallets: MutableStateFlow<State<UserWallets>> =
    MutableStateFlow(State.Init)

fun getUserWallets() {
    viewModelScope.launch {
        getUserWallets.getUserWallets()
            .onStart { _userWallets.value = State.Loading }
            .collect { _userWallets.value = it }
    }
}

我的测试:

    @Test
fun `observe user wallets ok`() = runBlockingTest {
    Mockito.`when`(api.getAssetWallets()).thenReturn(TestUtils.getAssetsWalletResponseOk())
    Mockito.`when`(api.getFiatWallets()).thenReturn(TestUtils.getFiatWalletResponseOk())

    viewModel.getUserWallets()

    val res = arrayListOf<State<UserWallets>>()
    viewModel.userWallet.toList(res) //doesn't works

    Assertions.assertThat(viewModel.userWallet.value is State.Success).isTrue() //works, last value enmited
}

访问发出的最后一个值有效。但我要测试的是所有发出的值都以正确的顺序发出。 使用这段代码:viewModel.userWallet.toList(res) //doesn't works 我收到以下错误:

java.lang.IllegalStateException: This job has not completed yet
    at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1189)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
    at WalletViewModelTest.observe user wallets ok(WalletViewModelTest.kt:52)
....

我想我遗漏了一些明显的东西。但不知道为什么,因为我刚刚开始使用 Coroutine 和 Flow,并且在不使用我已经使用的 runBlockingTest 时似乎会发生此错误。

编辑: 作为临时解决方案,我将其作为实时数据进行测试:

    @Captor
    lateinit var captor: ArgumentCaptor<State<UserWallets>>

    @Mock
    lateinit var walletsObserver: Observer<State<UserWallets>>

    @Test
    fun `observe user wallets ok`() = runBlockingTest {
        viewModel.userWallet.asLiveData().observeForever(walletsObserver)

        viewModel.getUserWallets()

        captor.run {
            Mockito.verify(walletsObserver, Mockito.times(3)).onChanged(capture())
            Assertions.assertThat(allValues[0] is State.Init).isTrue()
            Assertions.assertThat(allValues[1] is State.Loading).isTrue()
            Assertions.assertThat(allValues[2] is State.Success).isTrue()
        }
    }

【问题讨论】:

  • 在调用viewModel.getUserWallets() 之前,你放置这个断言:Assertions.assertThat(viewModel.userWallet.value is State.Init).isTrue() 并跳过这部分:val res = arrayListOf&lt;State&lt;UserWallets&gt;&gt;() viewModel.userWallet.toList(res)
  • 但我也在努力正确测试 StateFlow,我尝试使用 collectIndexed 但我遇到了同样的错误。也许是因为这个流程不会在runBlockingTest 块的末尾停止,我没有找到任何解决方案如何取消 StateFlow 以完成工作。我想测试它的正确方法是始终仅在发出一些更改后才测试该值。确保在 Dispatchers.Main 上发出这些
  • @executioner 我用临时解决方案更新了我的问题。哪个适用于现在
  • 嘿,我找到了一个解决方案:viewModel.userWallet.take(NUMBER_OF_EXPECTED_VALUES).collect { list.add(it) } 然后你可以测试列表值,就像 liveData 解决方案一样。
  • @executioner 太棒了!仍然有我得到“这个工作还没有完成”你在用 runBlockingTest 吗?

标签: android kotlin kotlin-coroutines android-viewmodel kotlin-flow


【解决方案1】:

SharedFlow/StateFlow 是一个热流,如文档中所述,A shared flow is called hot because its active instance exists independently of the presence of collectors. 这意味着启动流集合的范围不会自行完成。

要解决这个问题,你需要取消调用collect的范围,因为你的测试范围是测试本身,取消测试是不行的,所以你需要在不同的地方启动它工作。

@Test
fun `Testing a integer state flow`() = runBlockingTest{
    val _intSharedFlow = MutableStateFlow(0)
    val intSharedFlow = _intSharedFlow.asStateFlow()
    val testResults = mutableListOf<Int>()

    val job = launch {
        intSharedFlow.toList(testResults)
    }
    _intSharedFlow.value = 5

    assertEquals(2, testResults.size)
    assertEquals(0, testResults.first())
    assertEquals(5, testResults.last())
    job.cancel()
}

您的具体用例:

@Test
fun `observe user wallets ok`() = runBlockingTest {
    whenever(api.getAssetWallets()).thenReturn(TestUtils.getAssetsWalletResponseOk())
    whenever(api.getFiatWallets()).thenReturn(TestUtils.getFiatWalletResponseOk())

    viewModel.getUserWallets()

    val result = arrayListOf<State<UserWallets>>()
    val job = launch {
        viewModel.userWallet.toList(result) //now it should work
    }

    Assertions.assertThat(viewModel.userWallet.value is State.Success).isTrue() //works, last value enmited
    Assertions.assertThat(result.first() is State.Success) //also works
    job.cancel()
}

两件重要的事情:

  1. 总是取消你创建的工作以避免java.lang.IllegalStateException: This job has not completed yet
  2. 由于这是一个 StateFlow,当开始收集(在toList 内)时,您会收到最后一个状态。但是,如果您首先开始收集并在调用函数 viewModel.getUserWallets() 之后,然后在 result 列表中,您将拥有所有状态,以防您也想对其进行测试。

【讨论】:

  • 这是断言收集值的最终列表的好答案。相反,如果您需要测试行为,例如断言一个值的事件顺序是正确的,例如event(s) 1 -> value(s) 1 -> event(s) 2 -> value(s) 2 等,然后您需要将可测试单元分解为更小的单元(这可能很麻烦),或编写更多测试样板来测试所有这些。像github.com/cashapp/turbine 这样的测试库可以帮助交织事件和(无)值/错误/完成断言。
  • 你是对的,但最后你得到了所有的结果。这样您就可以检查是否按顺序接收了每个值。
【解决方案2】:

runBlockingTest 只是跳过您的情况下的延迟,但不会用您的测试调度程序覆盖 ViewModel 中使用的调度程序。您需要将TestCoroutineDispatcher 注入到您的ViewModel 中,或者由于您使用的是默认情况下已经使用Dispatchers.MainviewModelScope.launch {},您需要通过Dispatchers.setMain(testCoroutineDispatcher) 覆盖主调度程序。您可以创建以下规则并将其添加到您的测试文件中。

class MainCoroutineRule(
        val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
} 

在你的测试文件中

@get:Rule
var mainCoroutineRule = MainCoroutineRule()

@Test
fun `observe user wallets ok`() = mainCoroutineRule.testDispatcher.runBlockingTest {
}

顺便说一句,注入调度程序始终是一个好习惯。例如,如果您在协程范围内使用Dispatchers.Main 以外的调度程序,例如viewModelScope.launch(Dispatchers.Default),那么即使您使用的是测试调度程序,您的测试也会再次失败。原因是您只能使用Dispatchers.setMain() 覆盖主调度程序,因为它可以从其名称中理解,但不是Dispatchers.IODispatchers.Default。在这种情况下,您需要将mainCoroutineRule.testDispatcher 注入您的视图模型并使用注入的调度程序而不是对其进行硬编码。

【讨论】:

    【解决方案3】:

    【讨论】:

    • 这确实是 OP 看到“此作业尚未完成”错误的正确答案。所以解决方案是在一个单独的协程中收集流,并在 collect 函数中将值添加到一个可变列表中。然而,这仍然可能导致某些事件未被包括在内,因为如文档所述,如果收集器速度较慢,则会合并事件并跳过中间值。
    【解决方案4】:

    我们可以为given创建一个协程,为whenever

    创建一个协程

    无论何时编码,我们都可以使用yield,这样我们给定的代码就会完成并准备好断言!

    如您所见,您需要扩展 CouroutinScope:

    完成!

    • 您可以使用 emit 代替 tryEmit

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-04-20
      • 2018-05-13
      • 1970-01-01
      • 1970-01-01
      • 2021-02-25
      • 2019-08-23
      • 2022-07-05
      • 1970-01-01
      相关资源
      最近更新 更多