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

ViewModel中经stateIn转换的StateFlow单元测试不更新问题

问题描述

在使用存根仓库FakeMedicineRepository测试MedicinesViewModel时,调用仓库的addMedicine方法添加新条目后,uiState始终停留在Loading状态,无法更新为Success。仓库采用StateFlow存储模拟数据,并以普通Flow对外暴露,ViewModel将仓库返回的数据映射为MedicinesUiState类。替换仓库为SharedFlow后问题仍未解决。

FakeMedicinesRepository
class FakeMedicineRepository : MedicineRepository {

    private val _medicines = MutableStateFlow<List<Medicine>>(emptyList())
    override val allMedicines: Flow<List<Medicine>> = _medicines.asStateFlow()

    suspend fun emit(value: List<Medicine>) = _medicines.emit(value)

    override suspend fun addMedicine(
        name: String,
        purchasePrice: BigDecimal,
        sellingPrice: BigDecimal
    ) {
        _medicines.update {
            it.plus(
                Medicine(
                    it.size + 1L,
                    name,
                    purchasePrice,
                    sellingPrice
                )
            )
        }
    }

    override suspend fun isNameTaken(name: String): Boolean {
        return _medicines.value.any { it.name == name }
    }
}
MedicinesViewModel
class MedicinesViewModel(
    medicineRepository: MedicineRepository
) : ViewModel() {
    val uiState = medicineRepository.allMedicines
        .map {
            MedicinesUiState.Success(it.map { it.toUiState() })
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = MedicinesUiState.Loading
        )
}
MedicinesViewModelTest(原测试代码)
class MedicinesViewModelTest {

    private lateinit var repository: FakeMedicineRepository
    private lateinit var viewModel: MedicinesViewModel

    @Before
    fun setup() {
        repository = FakeMedicineRepository()
        viewModel = MedicinesViewModel(repository)
    }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun `when observe medicines should return empty list`() = runTest {
        backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
            viewModel.uiState.collect {
                println("State: $it")
            }
        }

        var uiState = viewModel.uiState.value
        uiState.shouldBeTypeOf<MedicinesUiState.Loading>()

        repository.addMedicine("Test", 0.toBigDecimal(), 0.toBigDecimal())

        // 此断言失败
        viewModel.uiState.value.shouldBeTypeOf<MedicinesUiState.Success>()
    }

}
解决方案

问题根源

  1. 流订阅状态问题:stateIn使用SharingStarted.WhileSubscribed(5000),当没有活跃的流收集器时,流会停止发射更新。测试中虽然启动了收集器,但可能因调度器时序问题导致收集器未及时保持活跃。
  2. 同步获取值的局限性:直接调用viewModel.uiState.value是同步获取当前快照,而addMedicine后的流更新是异步的,测试调度器未推进到更新完成的时间点,导致拿到的仍是旧状态。

修正后的测试代码

@OptIn(ExperimentalCoroutinesApi::class)
class MedicinesViewModelTest {

    private lateinit var repository: FakeMedicineRepository
    private lateinit var viewModel: MedicinesViewModel

    @Before
    fun setup() {
        repository = FakeMedicineRepository()
        viewModel = MedicinesViewModel(repository)
    }

    @Test
    fun `when observe medicines should return empty list`() = runTest {
        // 收集所有状态变化,确保流处于活跃订阅状态
        val collectedStates = mutableListOf<MedicinesUiState>()
        val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
            viewModel.uiState.collect { collectedStates.add(it) }
        }

        // 验证初始状态为Loading
        collectedStates.first().shouldBeTypeOf<MedicinesUiState.Loading>()

        // 添加药品
        repository.addMedicine("Test", 0.toBigDecimal(), 0.toBigDecimal())

        // 推进测试调度器,等待所有异步任务完成
        advanceUntilIdle()

        // 验证最终状态为Success,且包含新增的药品
        val finalState = collectedStates.last()
        finalState.shouldBeTypeOf<MedicinesUiState.Success>()
        (finalState as MedicinesUiState.Success).medicines.shouldHaveSize(1)
        (finalState.medicines.first().name shouldBe "Test")

        collectJob.cancel()
    }
}

额外优化建议

如果希望在测试中更稳定地观察流,可以临时调整ViewModel中stateIn的started参数为SharingStarted.Eagerly(仅测试环境),确保流始终处于活跃状态,避免因订阅超时导致的更新中断。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 15:08:12