Mockito模拟返回Flow的方法失败,触发ViewModel的catch块异常
问题:Mockito模拟返回Flow的方法时测试失败,catch块异常触发
尝试用Mockito模拟一个返回Flow的Repository方法,编写测试代码后出现以下问题:
- 测试失败,ViewModel中的catch块被触发抛出异常,导致LiveData的值为null
- 移除断言后,Mockito能验证被Mock的方法已被调用,测试通过
测试代码
package br.com.bmelocarvalho.mockitopoc import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.Assert.* import org.junit.Rule import org.junit.Test import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runner.RunWith import org.mockito.InjectMocks import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner @OptIn(ExperimentalCoroutinesApi::class) @RunWith(MockitoJUnitRunner::class) class MockitoTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule val changeMainDispatcherRule = ChangeMainDispatcherRule() @Mock lateinit var mockRepository: MiniRepository @InjectMocks lateinit var viewModel: MiniViewModel @Test fun `mock repository flow`() = runTest { whenever(mockRepository.getFlow()).thenReturn(flow { emit("fake service data") }) viewModel.data.observeForever {} val value = viewModel.data.value verify(mockRepository).getFlow() assertEquals("fake service data", value) } } class MiniViewModel( booksService: MiniRepository ) { val data: LiveData<String> = booksService.getFlow() .catch { throw RuntimeException("exception on flow emission") } .asLiveData() } class MiniRepository { fun getFlow() = flow { emit("service data") } } @OptIn(ExperimentalCoroutinesApi::class) class ChangeMainDispatcherRule( private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() } }
错误堆栈核心信息
Exception in thread "Test worker @coroutine#2" java.lang.RuntimeException: exception on flow emission ... expected:<fake service data> but was:<null> java.lang.AssertionError: expected:<fake service data> but was:<null> ...
原因分析
问题出在ViewModel的初始化时机早于Mock方法的Stub设置:
- 使用
@InjectMocks时,Mockito会在@Test方法执行前(JUnit4的初始化阶段)创建ViewModel实例 - 此时测试方法中
whenever(mockRepository.getFlow())的Stub逻辑还未执行,mockRepository的getFlow()返回的是Mockito生成的默认Mock Flow对象 - 这个Mock Flow在被
asLiveData()收集时会抛出异常,触发ViewModel中的catch块,导致LiveData从未收到发射的值,最终value为null - 移除断言后,Mockito的
verify仅检查方法是否被调用,不关心执行时的异常,因此测试能通过
解决方案
方案一:手动控制ViewModel初始化时机(推荐)
去掉@InjectMocks,在测试方法中先完成Mock Stub,再手动创建ViewModel:
@OptIn(ExperimentalCoroutinesApi::class) @RunWith(MockitoJUnitRunner::class) class MockitoTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule val changeMainDispatcherRule = ChangeMainDispatcherRule() @Mock lateinit var mockRepository: MiniRepository // 去掉@InjectMocks,手动初始化 lateinit var viewModel: MiniViewModel @Test fun `mock repository flow`() = runTest { // 先设置Stub whenever(mockRepository.getFlow()).thenReturn(flow { emit("fake service data") }) // 再创建ViewModel viewModel = MiniViewModel(mockRepository) viewModel.data.observeForever {} val value = viewModel.data.value verify(mockRepository).getFlow() assertEquals("fake service data", value) } }
方案二:提前在初始化阶段设置Stub
如果坚持使用@InjectMocks,可以在@Before方法中提前设置Stub(需确保Stub时机早于ViewModel初始化):
@OptIn(ExperimentalCoroutinesApi::class) @RunWith(MockitoJUnitRunner::class) class MockitoTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @get:Rule val changeMainDispatcherRule = ChangeMainDispatcherRule() @Mock lateinit var mockRepository: MiniRepository @InjectMocks lateinit var viewModel: MiniViewModel @Before fun setup() { // 在ViewModel初始化前设置Stub whenever(mockRepository.getFlow()).thenReturn(flow { emit("fake service data") }) } @Test fun `mock repository flow`() = runTest { viewModel.data.observeForever {} val value = viewModel.data.value verify(mockRepository).getFlow() assertEquals("fake service data", value) } }
内容的提问来源于stack exchange,提问作者bmelocarcalho
相关产品推荐
相关产品推荐

