Android单元测试emitAll返回的Flow报NoSuchElementException如何解决
问题背景
现有通过emitAll返回Flow的业务方法实现如下:
fun handle(actions: MoviesActions): Flow<MoviesStates> = flow { when (actions) { is MoviesActions.LoadMovies -> { emit(MoviesStates.Loading) emitAll(moviesUseCase.execute()) } } }
对应UseCase中的execute方法实现:
suspend fun execute(): Flow<MoviesStates> = flow { combine(f1, f2) { state1: MoviesStates, state2: MoviesStates -> // 业务处理逻辑 }.collect { emit(it) } }
测试时第一个发射值MoviesStates.Loading校验正常,但测试emitAll(moviesUseCase.execute())返回的流内容时测试失败,抛出异常:
java.util.NoSuchElementException: Expected at least one element
当前编写的单元测试代码:
@Test fun testLoadMovies() = runBlocking { whenever(useCase.execute()).thenReturn(flow { MoviesStates.EmptyList }) val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first() val expected = MoviesStates.EmptyList assertEquals(actual, expected) }
问题根因
测试失败的核心原因是Mock的UseCase返回的Flow没有发射任何值:
代码中flow { MoviesStates.EmptyList }只是在Flow构建块里写了MoviesStates.EmptyList的类引用,没有调用emit()方法把值推送到流中。调用drop(1).first()时,流丢弃第一个Loading状态后收集不到任何后续元素,就会抛出找不到元素的异常。
修复方案
1. 修正Mock的Flow逻辑
两种写法均可:
- 在Flow构建块中主动调用emit发射值
whenever(useCase.execute()).thenReturn(flow { emit(MoviesStates.EmptyList) })
- 直接使用
flowOf构建携带指定值的Flow,代码更简洁
whenever(useCase.execute()).thenReturn(flowOf(MoviesStates.EmptyList))
2. 可选代码优化
- 现有UseCase的
execute方法存在冗余包装:不需要手动收集combine结果再重新emit,直接返回combine生成的Flow即可,减少不必要的流嵌套,同时可以去掉多余的suspend修饰(combine本身返回冷流,不需要挂起):
fun execute(): Flow<MoviesStates> = combine(f1, f2) { state1: MoviesStates, state2: MoviesStates -> // 原有业务组合逻辑 }
- 协程单元测试推荐使用
runTest替代runBlocking,可以自动跳过协程延迟,提升测试执行效率。修正后的完整测试代码:
@Test fun testLoadMovies() = runTest { whenever(useCase.execute()).thenReturn(flowOf(MoviesStates.EmptyList)) val flowResult = viewModel.handle(MoviesActions.LoadMovies) val actual = flowResult.drop(1).first() assertEquals(MoviesStates.EmptyList, actual) }
内容的提问来源于stack exchange,提问作者Islam Ahmed
相关产品推荐
相关产品推荐

