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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 20:10:29