Android ViewModel单元测试:StateFlow的mapLatest未触发问题
问题根因
你的mapLatest逻辑不触发核心是协程调度和流启动时序的问题:
stateIn操作符绑定的协程上下文是viewModelScope,这个scope默认使用Dispatchers.Main调度器。本地单元测试环境没有Android主线程Looper,你也没有提前把主线程调度器替换为测试调度器,导致stateIn启动的上游流收集、mapLatest转换协程根本没有被调度执行,相当于整个流链没有正常启动。- 你给收集
viewModel.state的协程单独传了UnconfinedTestDispatcher,但这个调度器只作用于你自己写的收集协程,管不到viewModelScope里运行的流转换逻辑,完全不生效。 - 额外潜在问题:如果流刚启动还没完成对上游
service.state的订阅你就开始发射值,也会导致第一个发射的值被错过;加上distinctUntilChanged()的存在,如果两次发射的Disconnected实例equals判断相等,值还会被直接过滤。
修复方案
- 首先添加主线程调度器替换规则,测试执行前把主线程替换为测试调度器,测试结束后还原:
@OptIn(ExperimentalCoroutinesApi::class) class MainDispatcherRule( private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } }
在测试类中添加这个规则:
@get:Rule val mainDispatcherRule = MainDispatcherRule()
- 调整测试用例时序,启动收集协程后先做一次协程让步,确保
stateIn已经完成上游订阅、流链已经启动,再发射测试状态:
@Test fun testDeviceServiceDisconnectWithCauseMapsToUnableToConnect() = runTest { val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.state.collect() } // 等待流订阅完成 yield() deviceServiceState.emit( DeviceRemoteService.State.Disconnected(Exception("Something bad happened")) ) assertThat(viewModel.state.value).isInstanceOf(DeviceConfigurationModel.State.UnableToConnect::class.java) collectJob.cancel() }
- (可选排查)如果修复后断言还是失败,检查
DeviceRemoteService.State.Disconnected的equals实现:如果是sealed class的普通子类,要确保你初始化deviceServiceState用的带Exception的Disconnected实例,和后续emit的实例equals结果为false,否则distinctUntilChanged()会判定为重复值,不会向下游发送更新。
内容的提问来源于stack exchange,提问作者ExNatura
相关产品推荐
相关产品推荐

