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>() } }
解决方案
问题根源
- 流订阅状态问题:
stateIn使用SharingStarted.WhileSubscribed(5000),当没有活跃的流收集器时,流会停止发射更新。测试中虽然启动了收集器,但可能因调度器时序问题导致收集器未及时保持活跃。 - 同步获取值的局限性:直接调用
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
相关产品推荐
相关产品推荐

