Android ViewModel单元测试:Mockito模拟MutableStateFlow问题
解决ViewModel中MutableStateFlow模拟与单元测试问题
问题核心
你遇到的null问题是因为Mockito默认无法正确模拟Kotlin协程的MutableStateFlow类型,它会返回默认的null而非合法的流实例。要解决这个问题,我们需要用更适配Kotlin协程的测试工具,并手动控制流的发射值。
解决方案步骤
1. 引入必要测试依赖
在模块的build.gradle中添加协程测试、MockK(适配Kotlin的Mock工具)依赖:
testImplementation "io.mockk:mockk:1.13.8" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" testImplementation "junit:junit:4.13.2"
2. 编写测试类框架
使用MockK模拟依赖,用协程测试工具处理流的收集逻辑:
class ViewModelTest { // 模拟依赖 private val observeJoinDataUseCase: ObserveJoinDataUseCase = mockk() private val sharedPreferencesHelper: SharedPreferencesHelper = mockk(relaxed = true) // 测试专用调度器,确保协程在单线程执行 private val testDispatcher = StandardTestDispatcher() private lateinit var viewModel: ViewModel @Before fun setup() { // 替换Main线程调度器为测试调度器 Dispatchers.setMain(testDispatcher) // 模拟UseCase返回可控的MutableStateFlow,初始值设为Idle val testStateFlow = MutableStateFlow<State<DataClass>>(State.Idle) every { observeJoinDataUseCase.execute() } returns testStateFlow // 初始化ViewModel viewModel = ViewModel(observeJoinDataUseCase, sharedPreferencesHelper) } @After fun teardown() { // 恢复默认Main调度器 Dispatchers.resetMain() } }
3. 针对不同State分支编写测试用例
以下是覆盖所有分支的测试示例:
测试Success状态
@Test fun `handle Success state correctly`() = runTest(testDispatcher) { // 构造测试数据 val testStudent = Student(id = "test-user-id", micHardMuted = true) val testSession = Session(ended = false) val testData = DataClass(students = listOf(testStudent), session = testSession) val successState = State.Success(testData) // 获取模拟的StateFlow并发射Success状态 val stateFlow = observeJoinDataUseCase.execute() stateFlow.value = successState // 推进协程,确保流的收集逻辑执行完成 testDispatcher.scheduler.advanceUntilIdle() // 验证副作用:SharedPreferences被设置为false verify { sharedPreferencesHelper.audioEnabled = false } // 验证joinSession被调用(若joinSession是内部方法,可通过MockK的verify验证) // verify { viewModel.joinSession(testData.toPresentation()) } }
测试Loading状态
@Test fun `handle Loading state correctly`() = runTest(testDispatcher) { val loadingState = State.Loading val stateFlow = observeJoinDataUseCase.execute() stateFlow.value = loadingState testDispatcher.scheduler.advanceUntilIdle() // 验证screenLoadingSharedFlow发射了Loading状态 val testLoadingFlow = viewModel.screenLoadingSharedFlow as MutableSharedFlow<State> assertEquals(listOf(State.Loading), testLoadingFlow.receive()) }
测试Error状态
@Test fun `handle Error state correctly`() = runTest(testDispatcher) { val errorState = State.Error("test error") val stateFlow = observeJoinDataUseCase.execute() stateFlow.value = errorState testDispatcher.scheduler.advanceUntilIdle() // 验证apiErrorSharedFlow发射了Cancelled状态 val testErrorFlow = viewModel.apiErrorSharedFlow as MutableSharedFlow<State> assertEquals(listOf(State.Cancelled), testErrorFlow.receive()) }
测试忽略Idle/Cancelled/StopLoading状态
@Test fun `ignore idle, cancelled and stopLoading states`() = runTest(testDispatcher) { val stateFlow = observeJoinDataUseCase.execute() // 依次发射需要忽略的状态 listOf(State.Idle, State.Cancelled, State.StopLoading).forEach { state -> stateFlow.value = state testDispatcher.scheduler.advanceUntilIdle() } // 验证无任何副作用 verify(exactly = 0) { sharedPreferencesHelper.audioEnabled = any() } val testLoadingFlow = viewModel.screenLoadingSharedFlow as MutableSharedFlow<State> assertTrue(testLoadingFlow.receiveOrNull() == null) }
关键注意事项
- 避免直接用Mockito模拟
MutableStateFlow:Mockito对Kotlin协程类支持有限,MockK更适配Kotlin语法和协程逻辑。 - 协程测试必须用
runTest:确保挂起函数和流的收集逻辑在测试环境中正确执行。 - 用
TestDispatcher控制协程调度:避免并发问题,确保测试可重复、可预测。
内容的提问来源于stack exchange,提问作者arun
相关产品推荐
相关产品推荐

