Kotlin Flow抛出异常时单元测试中catch块未触发问题排查
问题分析与解决方案
核心排查方向
模拟器正常但单元测试中Flow异常未触发catch块,核心问题基本集中在Dispatcher替换不彻底、runTest的协程作用域匹配,以及Flow异常处理的细节上,具体看以下几点:
1. Dispatcher替换是否覆盖全链路
如果你的Repository/ViewModel直接硬编码Dispatchers.IO这类默认Dispatcher,测试时仅替换ViewModel层的Dispatcher,会导致部分异步代码跑在真实线程上,runTest无法捕获该线程的异常,进而让catch块看似未触发。
反例(错误写法):
// Repository直接使用硬编码Dispatcher,未做依赖注入 fun fetchData(): Flow<Result<Data>> = flow { val response = api.call() emit(Result.success(response)) }.catch { e -> cacheFailedRequest() emit(Result.failure(e)) }.flowOn(Dispatchers.IO)
修正方案:给所有需要异步执行的类注入Dispatcher,测试时统一替换为TestDispatcher:
// Repository通过构造注入Dispatcher class DataRepository( private val api: ApiService, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { fun fetchData(): Flow<Result<Data>> = flow { val response = api.call() emit(Result.success(response)) }.catch { e -> cacheFailedRequest() emit(Result.failure(e)) }.flowOn(dispatcher) } // 测试类实现 @OptIn(ExperimentalCoroutinesApi::class) class DataRepositoryTest { private val testDispatcher = UnconfinedTestDispatcher() private lateinit var repository: DataRepository @Before fun setup() { Dispatchers.setMain(testDispatcher) repository = DataRepository(FakeFailedApi(), testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun `fetch failure should cache request`() = runTest(testDispatcher) { val result = repository.fetchData().first() assertTrue(result is Result.Failure) // 验证缓存逻辑执行 } }
2. runTest的Dispatcher匹配问题
runTest默认使用StandardTestDispatcher,如果代码中混用了不同的TestDispatcher实例(比如Repository用UnconfinedTestDispatcher,测试用默认的StandardTestDispatcher),会导致异常抛出在测试协程作用域外,无法被Flow的catch捕获。
注意:确保所有异步代码的Dispatcher与runTest传入的Dispatcher为同一个实例,避免跨Dispatcher的异常传播失效。
3. Flow异常处理的位置与场景验证
catch块的位置:Flow的catch会捕获上游所有操作符的异常,但如果你的异常是在下游收集协程中抛出(比如ViewModel收集时主动抛出异常),catch块不会触发。比如:// ViewModel收集时主动抛异常,不会触发Repository的catch viewModelScope.launch { repository.fetchData().collect { result -> if (result is Result.Failure) throw result.exception } }- Fake依赖的异常触发逻辑:如果你的Fake Api是返回
Result.failure而非直接抛出异常,Flow的catch块根本不会触发——因为catch只捕获流中抛出的Throwable,不处理业务定义的Result失败类型。必须确保Fake Api直接抛出异常:// 正确的Fake Api:直接抛出异常 class FakeFailedApi : ApiService { override suspend fun call(): Data { throw IOException("Network error") } }
快速排查 Checklist
- 检查所有异步代码的Dispatcher是否都被
TestDispatcher替换,无硬编码默认Dispatcher - 确认
runTest使用的Dispatcher与代码中注入的Dispatcher为同一实例 - 验证Flow的
catch块是否覆盖了异常抛出的上游链路 - 确保测试用的Fake依赖是直接抛出异常,而非返回错误结果
内容的提问来源于stack exchange,提问作者Joe Dow
相关产品推荐
相关产品推荐

