android - 单元测试新的 Kotlin 协程 StateFlow

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

最近上课 StateFlow was introduced作为 Kotlin 协程的一部分。
我目前正在尝试它并在尝试对我的 ViewModel 进行单元测试时遇到问题。我想要实现的目标:测试我的 StateFlow 是否在我的 ViewModel 中以正确的顺序接收所有状态值。
我的代码如下。
View 模型:

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)我收到以下错误:
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)
....
我想我错过了一些明显的东西。但不知道为什么当我刚刚开始使用协程和 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()
    }
}

最佳答案

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()
}
两个重要的事情:
  • 始终取消您创建的工作以避免java.lang.IllegalStateException: This job has not completed yet
  • 由于这是一个 StateFlow,当开始收集(在 toList 内)时,您会收到最后一个状态。但是如果你第一次开始收集,然后你调用你的函数 viewModel.getUserWallets() ,然后在 result 内列表,您将拥有所有状态,以防您也想对其进行测试。
  • 关于android - 单元测试新的 Kotlin 协程 StateFlow,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62110761/

    相关文章:

    android - 如何在编译 APK(Release) 时排除文件夹

    java - Android & Dalvik - 获取对象的大小

    android - ClipboardManager OnPrimaryClipChangedListener 为每个副本调用两次

    android - DevicePolicyManager.resetPasswordWithToken 抛出 NPE

    kotlin - 在添加 “run”之前,将忽略Lambda。

    Kotlin:withContext() 与 Async-await

    android - 使用 launch() 范围时无法为 LiveData 变量赋值,但可以使用 runBlocking() 为 LiveData 赋值

    Kotlin 多平台 : How to start coroutine blockingly without runBlocking

    java - 有没有办法像我们在 laravel 中那样在 Android (java) 中使用 .env 变量?

    android - Activity 模糊导致滞后