使用Turbine测试ViewModel初始状态的最佳实践探讨
关于ViewModel状态初始化的Turbine测试最佳实践
问题背景
我有一个维护数据状态的简单ViewModel,状态在ViewModel的init函数中初始化:
ViewModelState.kt
data class ViewModelState( val myInfo: String = "", val mySecondInfo: String = "" )
MyViewModel.kt
class MyViewModel( private val client: Client ): ViewModel() { private val _state = MutableStateFlow(ViewModelState()) val state = _state.asStateFlow() init { loadMyInfoFromClient() loadMySecondInfoFromClient() } private fun loadMyInfoFromClient() { viewModelScope.launch { val retrievedData = client.suspendFunction() _state.update { it.copy(myInfo = retrievedData) } } } private fun loadMySecondInfoFromClient() { // 逻辑同loadMyInfoFromClient(),更新mySecondInfo } }
使用Turbine测试该ViewModel时,我目前用多个链式awaitItem()来等待状态发射并校验,但不够优雅:
fun `test state initialization`() = runTest { // Given prepareMockForClient() // When val underTest = MyViewModel(mockedClient) // Then underTest.state.test { val firstState = awaitItem() assertThat(firstState.myInfo).isEqualTo("blabla") val secondState = awaitItem() assertThat(secondState.mySecondInfo).isEqualTo("bleble") } }
我希望能直接等待最后一个发射的状态并校验完整值,类似这样的写法,但Turbine里没找到现成的awaitLastItem()方法:
underTest.state.test { val state = awaitLastItem() assertThat(state.myInfo).isEqualTo("blabla") assertThat(state.mySecondInfo).isEqualTo("bleble") }
除了自行实现awaitLastItem(),Android中测试ViewModel初始状态还有哪些更佳实践?
解决方案
1. 直接读取StateFlow的value(最简洁)
在runTest作用域中,ViewModel的viewModelScope会自动等待所有协程执行完成,所以可以直接读取state.value来获取最终状态:
fun `test state initialization with direct value`() = runTest { // Given prepareMockForClient() // When val underTest = MyViewModel(mockedClient) // Then assertThat(underTest.state.value.myInfo).isEqualTo("blabla") assertThat(underTest.state.value.mySecondInfo).isEqualTo("bleble") }
2. 使用Turbine的expectMostRecentItem()
Turbine提供了expectMostRecentItem()方法,可以获取流中最新发射的项(包括初始值),配合advanceUntilIdle()等待所有异步操作完成后使用:
fun `test state initialization with turbine expectMostRecentItem`() = runTest { // Given prepareMockForClient() // When val underTest = MyViewModel(mockedClient) // Then underTest.state.test { advanceUntilIdle() // 等待所有异步更新完成 val finalState = expectMostRecentItem() assertThat(finalState.myInfo).isEqualTo("blabla") assertThat(finalState.mySecondInfo).isEqualTo("bleble") } }
3. 优化ViewModel逻辑,减少状态发射次数
如果两个数据请求没有依赖关系,可以合并成一个协程并行请求,然后一次性更新状态,这样只会发射两次状态(初始空值+最终完整值),测试逻辑更简洁:
// 修改MyViewModel的init逻辑 init { viewModelScope.launch { val myInfoDeferred = async { client.suspendFunction() } val mySecondInfoDeferred = async { client.anotherSuspendFunction() } _state.update { it.copy( myInfo = myInfoDeferred.await(), mySecondInfo = mySecondInfoDeferred.await() ) } } }
对应的测试代码可以简化为:
fun `test merged state initialization`() = runTest { // Given prepareMockForClient() // When val underTest = MyViewModel(mockedClient) // Then underTest.state.test { awaitItem() // 跳过初始空状态 val finalState = awaitItem() assertThat(finalState.myInfo).isEqualTo("blabla") assertThat(finalState.mySecondInfo).isEqualTo("bleble") } }
内容的提问来源于stack exchange,提问作者Nadi-Shuddhi
相关产品推荐
相关产品推荐

