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

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设置:

  1. 使用@InjectMocks时,Mockito会在@Test方法执行前(JUnit4的初始化阶段)创建ViewModel实例
  2. 此时测试方法中whenever(mockRepository.getFlow())的Stub逻辑还未执行,mockRepository的getFlow()返回的是Mockito生成的默认Mock Flow对象
  3. 这个Mock Flow在被asLiveData()收集时会抛出异常,触发ViewModel中的catch块,导致LiveData从未收到发射的值,最终value为null
  4. 移除断言后,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 01:07:04