You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 09:13:18