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) } }
问题原因
- 初始化时机不匹配:ViewModel的
init块会在实例创建时立即触发请求(不管是直接调用fetchRandomFact()还是通过invokeFunction()),但测试中是先创建ViewModel,再设置Mockk应答,导致请求执行时没有可用的Mock配置,抛出找不到应答的错误。 - 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
相关产品推荐
相关产品推荐

