如何为包含SharedFlow的ViewModel编写单元测试用例
由于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
相关产品推荐
相关产品推荐

