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

如何为包含SharedFlow的ViewModel编写单元测试用例

解决SharedFlow单元测试无法捕获事件的问题

由于SharedFlow默认replayCache为0,测试时如果先触发事件再订阅流,会直接错过发射的事件。以下是几种可行的测试方案,无需修改原业务代码或仅做最小化调整:

方案1:提前订阅流,再触发操作

核心思路是在调用doSomething()前先启动协程订阅eventFlow,确保事件发射时能被捕获。使用kotlinx-coroutines-test的测试作用域来管理协程:

import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.Assert.assertEquals
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`

class ScreenVMTest {

    @Test
    fun `doSomething emits Event1 when getEvents returns Success`() = runTest(UnconfinedTestDispatcher()) {
        // Mock SomeClass,让其返回Success结果
        val mockSomeClass = mock(SomeClass::class.java)
        `when`(mockSomeClass.getEvents()).thenReturn(Success)

        // 创建ViewModel实例
        val vm = ScreenVM(mockSomeClass)

        // 提前启动协程订阅eventFlow,等待第一个事件
        val collectJob = launch {
            val emittedEvent = vm.eventFlow.first()
            assertEquals(ScreenVM.ScreenEvent.Event1, emittedEvent)
        }

        // 触发目标方法
        vm.doSomething()

        // 等待事件收集完成
        collectJob.join()
    }
}

方案2:使用test操作符简化事件捕获

kotlinx-coroutines-test提供的test操作符可以自动管理流的订阅,并记录所有发射的事件,用断言方法快速验证:

import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.flow.test
import org.junit.Test
import org.junit.Assert.assertEquals
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`

class ScreenVMTest {

    @Test
    fun `doSomething emits Event1 on Success response`() = runTest {
        // Mock依赖返回Success
        val mockSomeClass = mock(SomeClass::class.java)
        `when`(mockSomeClass.getEvents()).thenReturn(Success)

        val vm = ScreenVM(mockSomeClass)

        // 订阅流并生成TestCollector
        val testCollector = vm.eventFlow.test()
        
        // 触发业务方法
        vm.doSomething()

        // 验证发射的事件是否符合预期
        testCollector.assertValue(ScreenVM.ScreenEvent.Event1)
        
        // 结束测试,避免资源泄漏
        testCollector.cancel()
    }
}

方案3:测试时注入带replay的SharedFlow(可选)

如果不想处理订阅时机,可以通过构造注入替换原MutableSharedFlow,使用replay=1的实例让事件可以被后续订阅捕获。需要先微调原VM的构造:

// 原VM修改(仅为测试灵活性,不影响业务逻辑)
class ScreenVM(
    val someClass: SomeClass,
    private val _eventFlow: MutableSharedFlow<ScreenEvent> = MutableSharedFlow()
) : ViewModel {
    val eventFlow = _eventFlow.asSharedFlow()
    // ... 其余代码不变
}

测试代码:

import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.Assert.assertEquals
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import kotlinx.coroutines.flow.MutableSharedFlow

class ScreenVMTest {

    @Test
    fun `doSomething emits Event1`() = runTest {
        val mockSomeClass = mock(SomeClass::class.java)
        `when`(mockSomeClass.getEvents()).thenReturn(Success)

        // 创建带replay的SharedFlow实例
        val testEventFlow = MutableSharedFlow<ScreenVM.ScreenEvent>(replay = 1)
        val vm = ScreenVM(mockSomeClass, testEventFlow)

        // 先触发方法,再订阅流
        vm.doSomething()

        // 由于replay=1,first()可以获取到已发射的事件
        val emittedEvent = vm.eventFlow.first()
        assertEquals(ScreenVM.ScreenEvent.Event1, emittedEvent)
    }
}

推荐方案

优先选择方案1或方案2,无需修改原业务代码,符合测试隔离原则,核心是保证流的订阅在事件触发之前。

内容的提问来源于stack exchange,提问作者Mohit Trivedi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 20:05:17