Kotlin协程测试问题:Janitor类协程未执行完成
问题分析与修复方案
你的测试失败核心原因有两点:
- 调度器隔离:你给
Janitor传入了独立的StandardTestDispatcher实例,但测试中调用的advanceTimeBy和runCurrent仅作用于runTest默认提供的测试调度器,两者互不影响,导致Janitor内的协程始终处于delay等待状态。 - 协程作用域生命周期未绑定:
Janitor自行创建的CoroutineScope未与测试作用域关联,测试结束时框架可能直接终止未完成的协程,无法保证清理逻辑执行完毕。
修复方案一:让Janitor复用测试作用域(推荐)
这是协程开发的最佳实践——避免组件自行创建作用域,由外部传入以统一管理生命周期。
修改Janitor类:
class Janitor ( private val sharedMap: MutableMap<Number, String>, private val delayMs: Long, private val scope: CoroutineScope, // 改为接收外部作用域 ) { fun cleanUp(key: Number) { scope.launch(CoroutineName("Janitor Cleaning Up key:[$key]")) { println("Launching coroutine to clean up $key after $delayMs") delay(delayMs) sharedMap.remove(key) println("Done removing $key from sharedMap after $delayMs") } } }
修改测试代码,将runTest的测试作用域传给Janitor:
class JanitorTest { @Test fun `janitor cleans up`() = runTest { val sharedMap = mutableMapOf<Number, String>() // 传递测试作用域给Janitor val janitor = Janitor(sharedMap, 1000L, this) sharedMap[1] = "hello" sharedMap[2] = "world" janitor.cleanUp(1) advanceTimeBy(1000L) runCurrent() // 验证结果 check(sharedMap.size == 1) check(!sharedMap.containsKey(1)) } }
修复方案二:操作Janitor使用的调度器(不修改Janitor结构)
如果不想调整Janitor的构造,可以让测试直接控制Janitor内部使用的调度器:
先将Janitor的dispatcher改为可访问的属性:
class Janitor ( private val sharedMap: MutableMap<Number, String>, private val delayMs: Long, val dispatcher: CoroutineDispatcher = newSingleThreadContext("JanitorThread"), // 改为val暴露 ) { private val janitorScope = CoroutineScope(dispatcher) fun cleanUp(key: Number) { janitorScope.launch(CoroutineName("Janitor Cleaning Up key:[$key]")) { println("Launching coroutine in JanitorThread to clean up $key after $delayMs") delay(delayMs) sharedMap.remove(key) println("Done removing $key from sharedMap after $delayMs") } } }
然后在测试中直接操作该调度器的时间推进:
class JanitorTest { @Test fun `janitor cleans up`() = runTest { val sharedMap = mutableMapOf<Number, String>() val testDispatcher = StandardTestDispatcher(testScheduler) val janitor = Janitor(sharedMap, 1000L, testDispatcher) sharedMap[1] = "hello" sharedMap[2] = "world" janitor.cleanUp(1) // 直接操作Janitor使用的调度器 testDispatcher.advanceTimeBy(1000L) testDispatcher.runCurrent() check(sharedMap.size == 1) } }
额外注意事项
- 避免在生产代码中自行创建
newSingleThreadContext,这类线程池需要手动关闭,容易造成资源泄漏,推荐使用Dispatchers.IO等内置调度器。 - 生产环境中,
Janitor的作用域应绑定到组件的生命周期(比如Android的ViewModelScope),防止内存泄漏。
内容的提问来源于stack exchange,提问作者user2583310
相关产品推荐
相关产品推荐

