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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 03:00:57