如何对包含多个async调用的Kotlin协程进行单元测试
问题根因
你遇到的返回值不符合预期的问题由两个核心原因导致:
- MockK的单
slot捕获器不是线程安全的:slot内部仅用单个字段存储捕获到的参数,当两个协程在Dispatchers.Default线程池并发执行时,第二个协程调用dependency1.func1时会覆盖第一个协程存入slot的参数值。你在answers代码块中读取slot1.captured时,拿到的不一定是当前调用对应的入参,自然会返回错误的结果。 - 待测试代码硬编码了
Dispatchers.Default协程调度器:这种写法既破坏了代码的可测试性,也让单元测试无法控制协程的执行时序,容易引入偶发的并发时序问题。
修正步骤
1. 重构待测试类,注入协程调度器
不要在业务代码中硬编码协程调度器,将其作为构造参数传入(生产环境可以给默认值,不影响原有业务逻辑),方便测试时替换调度器消除并发干扰:
class ClassToTest( val dependency1: Dependency1, val dependency2: Dependency2, // 生产环境默认使用Dispatchers.Default,测试时可替换 private val ioDispatcher: CoroutineDispatcher = Dispatchers.Default ) { fun funToTest(list: List<X>) { runBlocking { val deferreds: List<Deferred<Type>> = list.map { item -> // 替换硬编码的调度器为构造传入的实例 async(ioDispatcher) { val v1 = dependency1.func1(item) val v2 = dependency2.func2(v1) someFunc(v2) } } val results = deferreds.awaitAll() } } }
2. 修正Mock逻辑,弃用单slot做入参判断
你不需要用slot捕获入参来做返回值分支判断,MockK的answers作用域本身就可以直接获取当前调用的入参,这个入参是和单次调用绑定的,完全不会被其他并发调用覆盖:
// 移除原有slot定义,直接在answers中获取当前调用的入参 every { dependency1.func1(any()) } answers { // firstArg<X>() 代表当前这次func1调用传入的第一个参数,即对应的item val currentItem = firstArg<X>() if (currentItem.field == xxx) { return1 } else { return2 } } // dependency2的Mock逻辑和上述写法一致,直接从answers作用域取当前调用入参判断即可
如果后续你确实需要校验所有调用的入参,不要用单个slot,改用可变列表存储每次捕获的参数即可:
val capturedParams = mutableListOf<X>() every { dependency1.func1(capture(capturedParams)) } answers { val currentItem = firstArg<X>() // 分支判断逻辑同上 } // 测试执行完成后可以遍历capturedParams做参数校验
3. 测试时替换调度器消除并发干扰
初始化待测试类时,传入Dispatchers.Unconfined或者kotlinx-coroutines-test提供的测试调度器,让协程在当前测试线程同步执行,彻底避免并发时序导致的偶发问题:
val testedClass = ClassToTest( dependency1 = mockDependency1, dependency2 = mockDependency2, // 测试时用无约束调度器,协程同步执行 ioDispatcher = Dispatchers.Unconfined ) // 正常调用待测试方法即可 testedClass.funToTest(testListOf2Items)
内容的提问来源于stack exchange,提问作者Ambrosia Mojito
相关产品推荐
相关产品推荐

