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

测试中如何收集StateFlow全部发射值?解决协程未完成异常

问题分析与解决方案

问题根源

测试失败的核心原因有两点:

  1. ViewModel中硬编码使用Dispatchers.IO,测试时未替换为测试调度器,导致这部分协程脱离runTest的管控,断言执行时状态更新逻辑还未完成。
  2. 测试代码未等待状态流的所有发射动作完成就执行断言,同时未被管控的后台协程最终触发UncompletedCoroutinesError。

修复方案

1. 改造ViewModel,支持调度器注入(推荐)

将调度器作为参数传入ViewModel,避免硬编码,方便测试时替换:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

class TestViewmodel(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
): ViewModel() {
    private val _event = MutableSharedFlow<TestEvent>()
    private val _state = MutableStateFlow(TestState())
    val state = _state.asStateFlow()

    init {
        viewModelScope.launch {
            _event.collect{ handleEvent(it) }
        }
    }

    private fun handleEvent(event: TestEvent){
        if(event == TestEvent.GetData) getTestData()
    }

    private fun getTestData(){
        viewModelScope.launch(ioDispatcher){
            _state.update { it.copy(loading = true) }
            val data = getApiData()
            _state.update { it.copy(loading = false) }
            handleApiData(data)
        }
    }

    private fun handleApiData(list: List<String>){
        _state.update { it.copy(data = list) }
    }

    private suspend fun getApiData(): List<String>{
        delay(1000)
        return listOf("ffefe","ffefe","fdfdfd")
    }

    fun setEvent(event: TestEvent){
        viewModelScope.launch { _event.emit(event) }
    }
}

data class TestState(
    val loading: Boolean = false,
    val data: List<String> = emptyList()
)

sealed interface TestEvent{
    data object GetData: TestEvent
}

2. 修复测试代码,用TestDispatcher管控所有协程

将所有Dispatchers替换为测试调度器,确保runTest能等待所有协程任务完成:

import io.kotest.matchers.shouldBe
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance

@OptIn(ExperimentalCoroutinesApi::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestViewModelTest(){
    private lateinit var viewModel: TestViewmodel
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()

    @BeforeAll
    fun setUp(){
        Dispatchers.setMain(testDispatcher)
        viewModel = TestViewmodel(ioDispatcher = testDispatcher)
    }

    @AfterAll
    fun afterAll(){
        Dispatchers.resetMain()
    }

    @Test
    fun testEmit() = runTest {
        viewModel.setEvent(TestEvent.GetData)
        // 跳过初始状态+2次中间更新,取最终状态
        val result = viewModel.state.drop(3).first()
        result.data.size shouldBe 3
    }

    @Test
    fun testEmitWithTurbine() = runTest {
        viewModel.state.test {
            viewModel.setEvent(TestEvent.GetData)
            
            awaitItem() // 跳过初始状态
            awaitItem().loading shouldBe true // 验证loading启动
            awaitItem().loading shouldBe false // 验证loading结束
            awaitItem().data.size shouldBe 3 // 验证数据填充完成
        }
    }

    @Test
    fun testEmitByGoogle() = runTest {
        val result = mutableListOf<TestState>()
        val collectionJob = launch { viewModel.state.collect { result.add(it) } }

        viewModel.setEvent(TestEvent.GetData)
        testDispatcher.scheduler.advanceUntilIdle() // 等待所有协程任务完成
        
        result.last().data.size shouldBe 3
        collectionJob.cancel()
    }
}

3. 关键注意事项

  • 全调度器替换:必须替换ViewModel中用到的所有Dispatcher(包括IO),否则后台协程会脱离测试管控。
  • advanceUntilIdle():强制测试调度器执行完所有待处理任务,确保状态更新全部完成后再执行断言。
  • 状态发射次数:初始状态是第一次发射,后续每次update都会产生新的发射值,总共会有4次发射(初始+3次更新)。

内容的提问来源于stack exchange,提问作者Andreas1234

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 05:50:24