Kotest测试环境中自定义CoroutineContext.Element获取为null的原因
我遇到一个协程上下文传播的问题:在测试与生产环境下,以及协程内不同的上下文访问方式中,获取自定义CoroutineContext.Element(TraceContextElement)的结果存在差异,且该问题和MDCContext相关。
目标:在协程间传播自定义TraceContextElement,包括从主请求线程启动的异步"即发即弃"任务,同时通过kotlinx-coroutines-slf4j维护MDC上下文。
代码设置:
- 自定义上下文元素
import com.kakao.zertfds.infrastructure.logger.domain.TraceContext // 简化后的TraceContext import kotlin.coroutines.AbstractCoroutineContextElement import kotlin.coroutines.CoroutineContext // TraceContext的包装类 class TraceContextElement(val context: TraceContext) : AbstractCoroutineContextElement(Key) { companion object Key : CoroutineContext.Key<TraceContextElement> } // 获取上下文的扩展方法 fun CoroutineContext.traceContext(): TraceContext? = this[TraceContextElement]?.context // 简化的TraceContext数据类 data class TraceContext(val traceId: String?, /* 其他字段 */ )
- 协程启动工具函数
我有两个工具函数,用于启动协程时添加MDCContext和TraceContextElement:
import kotlinx.coroutines.* import kotlinx.coroutines.slf4j.MDCContext import org.slf4j.MDC import kotlin.coroutines.coroutineContext // --- 启动器1:使用CoroutineScope扩展lambda --- fun CoroutineScope.launchWithTrace( traceManager: TraceManager, // 测试中已Mock block: suspend CoroutineScope.() -> Unit // lambda是CoroutineScope的扩展 ): Job { val mdcContextMap = MDC.getCopyOfContextMap() ?: emptyMap() val traceContext = traceManager.createTraceContext() // 返回测试用的TraceContext // 携带两个上下文启动协程 return launch(MDCContext() + TraceContextElement(traceContext)) { try { if (mdcContextMap.isNotEmpty()) MDC.setContextMap(mdcContextMap) block.invoke(this) // `this`是当前CoroutineScope } finally { MDC.clear() } } } // --- 启动器2:使用标准挂起lambda --- fun CoroutineScope.launchWithTrace2( traceManager: TraceManager, // 测试中已Mock block: suspend () -> Unit // lambda是标准挂起函数 ): Job { val mdcContextMap = MDC.getCopyOfContextMap() ?: emptyMap() val traceContext = traceManager.createTraceContext() // 返回测试用的TraceContext // 携带两个上下文启动协程 return launch(MDCContext() + TraceContextElement(traceContext)) { try { if (mdcContextMap.isNotEmpty()) MDC.setContextMap(mdcContextMap) block() // 直接调用标准函数 } finally { MDC.clear() } } }
(假设TraceManager是提供createTraceContext(): TraceContext方法的类)
问题现象:
在使用Dispatchers.Default的Kotest测试环境中:
- 使用
launchWithTrace(CoroutineScope扩展lambda):
test("Test with launchWithTrace") { val scope = CoroutineScope(Dispatchers.Default) val mockTraceManager = mockk<TraceManager>() every { mockTraceManager.createTraceContext() } returns TraceContext("test-id") val latch = CountDownLatch(1) scope.launchWithTrace(mockTraceManager) { // 使用扩展lambda // 通过隐式的`this.coroutineContext`访问上下文 val ctx = coroutineContext.traceContext() println("launchWithTrace context: $ctx") // 输出: launchWithTrace context: TraceContext(traceId=test-id) ctx shouldNotBe null // 断言通过 latch.countDown() } latch.await(2, TimeUnit.SECONDS) shouldBe true }
此场景符合预期,能通过coroutineContext成功获取TraceContextElement。
- 使用
launchWithTrace2(标准挂起lambda):
test("Test with launchWithTrace2 - FAILS") { val scope = CoroutineScope(Dispatchers.Default) val mockTraceManager = mockk<TraceManager>() every { mockTraceManager.createTraceContext() } returns TraceContext("test-id") val latch = CountDownLatch(1) scope.launchWithTrace2(mockTraceManager) { // 使用标准lambda // 通过顶层的`kotlin.coroutines.coroutineContext`访问上下文 val ctx = kotlin.coroutines.coroutineContext.traceContext() println("launchWithTrace2 context: $ctx") // 输出: launchWithTrace2 context: null // ctx shouldNotBe null // 该断言会失败 latch.countDown() // 如果添加断言可能无法执行到这里 } latch.await(2, TimeUnit.SECONDS) shouldBe true // 测试可能挂起或断言失败 }
此场景失败,通过kotlin.coroutines.coroutineContext获取TraceContextElement返回null。
但在JVM上运行的Spring Boot生产环境中:
- 在控制器或服务中使用
launchWithTrace2(通常搭配Dispatchers.IO的CoroutineScope),通过kotlin.coroutines.coroutineContext能正确获取TraceContextElement。
疑问:
为什么测试环境中,标准suspend () -> Unit lambda内通过kotlin.coroutines.coroutineContext获取TraceContextElement会失败,但生产环境正常,且测试环境中使用CoroutineScope扩展lambda也正常?这是否和kotlin.coroutines.coroutineContext与MDCContext的交互、Dispatchers.Default在测试框架中的线程切换有关?是否存在Continuation与CoroutineScope对象上下文解析的细节问题?
内容的提问来源于stack exchange,提问作者happy_friend

