单元测试新的 Kotlin 协程 StateFlow [英] Unit test the new Kotlin coroutine StateFlow

查看:61
本文介绍了单元测试新的 Kotlin 协程 StateFlow的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

最近介绍StateFlow 作为 Kotlin 协程的一部分.

Recently was introduced the class StateFlow as part of Kotlin coroutine.

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

I'm currently trying it and encounter an issue while trying to unit test my ViewModel. What I want to achieve: testing that my StateFlow is receiving all the state values in the correct order in my 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)//不工作我收到以下错误:

Accessing the last value emitted works. But What I want to test is that all the emitted values are emitted in the correct order. with this piece of code: viewModel.userWallet.toList(res) //doesn't works I'm getting the following error :

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 时似乎会发生此错误.

I guess I'm missing something obvious. But not sure why as I'm just getting started with Coroutine and Flow and this error seems to happen when not using runBlockingTest, which I use already.

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

As a temporary solution, I'm testing it as a live data:

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

推荐答案

SharedFlow/StateFlow 是一个热流,如文档中所述,共享流被称为热流,因为它的活动实例独立于存在而存在收集器的数量.这意味着,启动您的流的集合的范围不会自行完成.

SharedFlow/StateFlow is a hot flow, and as described in the docs, A shared flow is called hot because its active instance exists independently of the presence of collectors. It means, the scope that launches the collection of your flow won't complete by itself.

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

To solve this issue you need to cancel the scope in which the collect is called, and as the scope of your test is the test itself, its not ok to cancel the test, so what you need is launch it in a different job.

@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:此作业尚未完成
  2. 由于这是一个 StateFlow,当开始收集时(在 toList 内)你会收到最后一个状态.但是,如果您首先开始收集并在调用函数 viewModel.getUserWallets() 之后,则在 result 列表中,您将拥有所有状态,以防万一也测试一下.
  1. Always cancel your created job to avoid java.lang.IllegalStateException: This job has not completed yet
  2. As this is a StateFlow, when start collecting (inside toList) you receive the last state. But if you first start collecting and after you call your function viewModel.getUserWallets(), then inside the result list, you will have all the states, in case you want to test it too.

这篇关于单元测试新的 Kotlin 协程 StateFlow的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆