最近,类StateFlow
was introduced作为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)
出现以下错误:
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()
}
}
7条答案
按热度按时间vdgimpew1#
SharedFlow/StateFlow是一个热流,如文档中所述,
A shared flow is called hot because its active instance exists independently of the presence of collectors.
这意味着启动流集合的作用域不会自行完成。要解决这个问题,你需要取消调用collect的作用域,因为测试的作用域是测试本身,所以取消测试是不对的,所以你需要在不同的作业中启动它。
您的具体使用案例:
两件重要的事:
1.始终取消已创建的作业以避免
java.lang.IllegalStateException: This job has not completed yet
1.由于这是一个StateFlow,当开始收集(在
toList
内部)时,您将收到最后一个状态。但是如果你第一次开始收集数据,然后调用函数viewModel.getUserWallets()
,那么在result
列表中,你将拥有所有的状态,以防你也想测试它。dkqlctbz2#
我从Kotlin协同程序GitHub存储库中的解决方案中得出的另一种方法:
这是我的 ViewModel 类:
请注意,我使用的是Kotlin 1.6.10 和kotlinx.coroutines-test 1.6.1:
另外,请参阅官方的Kotlin协程migration guide to the new test API。
amrnrhlw3#
runBlockingTest
只是跳过用例中的延迟,但不会用测试调度器覆盖ViewModel中使用的调度器。你需要将TestCoroutineDispatcher
注入到你的ViewModel中,或者因为你使用的是viewModelScope.launch {}
,而默认情况下它已经使用了Dispatchers.Main
,你需要通过Dispatchers.setMain(testCoroutineDispatcher)
覆盖主调度器。您可以创建以下规则并将其添加到测试文件中。在你的测试文件里
顺便说一句,注入调度程序总是一个很好的做法。例如,如果您在协同程序作用域(如
viewModelScope.launch(Dispatchers.Default)
)中使用了Dispatchers.Main
以外的调度程序,那么即使您使用了测试调度程序,测试也会再次失败。原因是你只能用Dispatchers.setMain()
覆盖主调度器,因为它可以从它的名字中理解,而不是Dispatchers.IO
或Dispatchers.Default
。在这种情况下,您需要将mainCoroutineRule.testDispatcher
注入到视图模型中,并使用注入的分派器,而不是硬编码它。mklgxw1f4#
您面临的问题是因为toList()需要流来完成,而根据文档,“状态流永远不会完成”。
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/to-list.html
roqulrg35#
我们可以为given创建一个协程,为whenever创建一个协程
在whenever代码之后,我们可以使用yield,这样我们给定的代码就完成了,可以Assert了!
要做到这一点,你需要扩展CouroutinScope,如你所见:
搞定!
mrphzbgm6#
这就是我正在使用的(不需要自定义VM调度器):
使用
launchIn
和advanceUntilIdle
可以解决您的测试问题。mf98qq947#
通过一些小的改进来使用它https://github.com/Kotlin/kotlinx.coroutines/issues/3143#issuecomment-1097428912
我现在可以在测试中执行以下操作
我的ViewModel