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

Android ViewModel+StateFlow测试问题:测试无法等待后续值

搜索ViewModel单元测试失败问题解决

问题背景

我实现了一个搜索ViewModel,其中searchPoiUseCase负责与Room数据库交互发起查询请求,测试阶段使用Room.inMemoryDatabaseBuilder创建内存数据库。

ViewModel代码如下:

@HiltViewModel
class SearchVm @Inject constructor(
    private val searchPoiUseCase: SearchPoiUseCase
) : ViewModel() {

    private val queryState = MutableStateFlow("")
    
    @OptIn(FlowPreview::class)
    val searchScreenState = queryState
        .filter { it.isNotEmpty() }
        .debounce(500)
        .distinctUntilChanged()
        .map { query -> searchPoiUseCase(SearchPoiUseCase.Params(query)) }
        .map { result ->
            if (result.isEmpty()) SearchScreenUiState.NothingFound
            else SearchScreenUiState.SearchResult(result.map { it.toListUiModel() })
        }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            SearchScreenUiState.None
        )
    
    fun onSearch(query: String) {
        queryState.value = query
    }
}

这段逻辑在真机/模拟器上运行完全正常,但单元测试始终无法通过。以下是我的单元测试代码:

@OptIn(ExperimentalCoroutinesApi::class)
@HiltAndroidTest
@Config(application = HiltTestApplication::class)
@RunWith(RobolectricTestRunner::class)
class SearchViewModelTest {

    @get:Rule
    var hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var searchUseCase: SearchPoiUseCase

    lateinit var SUT: SearchVm

    @Before
    fun setup() {
        hiltRule.inject()
        SUT = SearchVm(searchUseCase)
        Dispatchers.setMain(UnconfinedTestDispatcher())
    }

    @After
    fun teardown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `test search view model`() = runTest {
        val collectJob = launch { SUT.searchScreenState.collect() }
        assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)

        SUT.onSearch("Query")
        assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)

        collectJob.cancel()
    }
}

测试中的第二个断言始终失败,请问我遗漏了什么?


解决方案

感谢Ibrahim Disouki的思路,我对测试代码做了一处修改后问题解决,修改后的测试代码如下:

@Test
fun `test search view model`() = runTest {
    whenever(searchUseCase(SearchPoiUseCase.Params("Query"))).thenReturn(emptyList()) // 此处可替换为返回有效数据的测试用例

    assertEquals(SearchScreenUiState.None, SUT.searchScreenState.value)

    val job = launch {
        SUT.searchScreenState.collect() // 确保流被收集
    }

    SUT.onSearch("Query")
    advanceTimeBy(500) // 跳过debounce设置的500ms延迟
    runCurrent() // 执行测试调度器中当前虚拟时间的所有待处理任务

    assertEquals(SearchScreenUiState.NothingFound, SUT.searchScreenState.value)

    job.cancel()
}

核心修改点:

  • 为searchUseCase设置模拟返回值,避免依赖真实数据库
  • 调用advanceTimeBy(500)跳过ViewModel中debounce(500)的延迟等待
  • 调用runCurrent()确保所有待处理的流操作都执行完毕

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 02:15:13