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

Android中Suspend函数与StateFlow单元测试报错排查

问题:Mockk模拟UseCase时出现「no answer found」错误

我正在为Android应用编写单元测试,用Mockk模拟类,但测试运行失败,报错找不到应答。以下是相关代码:

测试代码

@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModalTest {

    private lateinit var viewModel: MainViewModal
    private val savedStateHandle = mockk<SavedStateHandle>(relaxed = true)
    private val randomFactUseCase = mockk<RandomFactUseCase>()

    @get:Rule
    val mainDispatcher = MainDispatcherRule()

    @Before
    fun setup() {
        viewModel = MainViewModal(randomFactUseCase, savedStateHandle)
    }

    @Test
    fun `When fetchRandomFact succeeds, update UI state with fact`() = runBlockingTest {
        // Given
        coEvery { randomFactUseCase.invoke() } returns Result.Success(RandomFact(data = listOf("Some interesting fact")), 200)

        // When
        viewModel.fetchRandomFact()

        // Then
        val expectedUiState = UiState(loading = false, fact = "Some interesting fact", error = "")
        assert(viewModel.uiState.value == expectedUiState)
    }
}

class MainDispatcherRule(private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()) :
    TestWatcher() {

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

报错信息

Exception in thread "Test worker @coroutine#1"
io.mockk.MockKException: no answer found for
RandomFactUseCase(#2).invoke(continuation {}) among the configured
answers: ()

原ViewModel代码

@HiltViewModel
class MainViewModal @Inject constructor(
    private val randomFactUseCase: RandomFactUseCase,
    private val savedStateHandle: SavedStateHandle,

    ) : ViewModel() {
    init {
        fetchRandomFact()
    }

    val uiState: StateFlow<UiState> = savedStateHandle.getStateFlow("fact", UiState(loading = true))

    fun fetchRandomFact() {
        viewModelScope.launch {
            setState(loading = true)
            when (val response = randomFactUseCase()) {
                is Result.Success -> {
                    val fact =
                        if (response.data?.data?.isNotEmpty() == true) response.data.data[0] else ""
                    setState(
                        loading = false,
                        fact = fact,
                        error = if (fact.isBlank()) "No fact available!" else ""
                    )
                }

                is Result.Error -> {
                    setState(
                        loading = false,
                        fact = "",
                        error = response.exception.message
                            ?: "Something went wrong! Please try again"
                    )
                }
            }
        }
    }

    private fun setState(loading: Boolean = true, fact: String = "", error: String = "") {
        savedStateHandle["fact"] = UiState(loading = loading, fact = fact, error = error)
    }
}

@Parcelize
data class UiState(val loading: Boolean = true, val fact: String = "", val error: String = "") :
    Parcelable

修改后的ViewModel代码(按建议调整后仍报错)

@HiltViewModel
class MainViewModal @Inject constructor(
    private val randomFactUseCase: RandomFactUseCase,
    private val savedStateHandle: SavedStateHandle,

    ) : ViewModel() {
    init {
        invokeFunction()
    }

    fun invokeFunction() {
        viewModelScope.launch {
            fetchRandomFact()
        }
    }

    val uiState: StateFlow<UiState> = savedStateHandle.getStateFlow("fact", UiState(loading = true))

    suspend fun fetchRandomFact() {
        setState(loading = true)
        when (val response = randomFactUseCase()) {
            is Result.Success -> {
                val fact =
                    if (response.data?.data?.isNotEmpty() == true) response.data.data[0] else ""
                    setState(
                        loading = false,
                        fact = fact,
                        error = if (fact.isBlank()) "No fact available!" else ""
                    )
            }

            is Result.Error -> {
                setState(
                    loading = false,
                    fact = "",
                    error = response.exception.message ?: "Something went wrong! Please try again"
                )
            }
        }
    }

    private fun setState(loading: Boolean = true, fact: String = "", error: String = "") {
        savedStateHandle["fact"] = UiState(loading = loading, fact = fact, error = error)
    }
}

问题原因

  1. 初始化时机不匹配:ViewModel的init块会在实例创建时立即触发请求(不管是直接调用fetchRandomFact()还是通过invokeFunction()),但测试中是先创建ViewModel,再设置Mockk应答,导致请求执行时没有可用的Mock配置,抛出找不到应答的错误。
  2. Mock匹配语法不一致:测试中用randomFactUseCase.invoke()设置应答,但ViewModel里实际调用的是randomFactUseCase(),虽然语法等价,但Mockk的匹配逻辑可能因此不生效。

修复方案

1. 调整测试初始化顺序

在创建ViewModel实例之前设置Mockk应答,确保请求执行时已有配置:

@Before
fun setup() = runBlockingTest {
    // 先配置Mock应答
    coEvery { randomFactUseCase() } returns Result.Success(RandomFact(data = listOf("Some interesting fact")), 200)
    // 再创建ViewModel
    viewModel = MainViewModal(randomFactUseCase, savedStateHandle)
}

2. 统一Mock匹配语法

把测试中的coEvery { randomFactUseCase.invoke() }改成coEvery { randomFactUseCase() },和ViewModel中的调用语法保持完全一致,避免匹配失败。

3. (可选)控制ViewModel初始化行为

如果不想让ViewModel自动触发请求,可以把初始化逻辑移到外部,比如暴露initData()方法由UI层调用,测试时可以手动控制触发时机:

// ViewModel中修改
@HiltViewModel
class MainViewModal @Inject constructor(
    private val randomFactUseCase: RandomFactUseCase,
    private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
    // 去掉init块,改为外部调用
    fun initData() {
        viewModelScope.launch {
            fetchRandomFact()
        }
    }

    // ... 其他代码不变
}

// 测试中修改
@Test
fun `When fetchRandomFact succeeds, update UI state with fact`() = runBlockingTest {
    // Given
    coEvery { randomFactUseCase() } returns Result.Success(RandomFact(data = listOf("Some interesting fact")), 200)
    viewModel = MainViewModal(randomFactUseCase, savedStateHandle)

    // When
    viewModel.initData()

    // Then
    val expectedUiState = UiState(loading = false, fact = "Some interesting fact", error = "")
    assert(viewModel.uiState.value == expectedUiState)
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 07:08:15