Scala中使用OpenTelemetry Mock追踪时测试异常排查求助
问题描述
Scala项目中使用OpenTelemetry实现链路追踪,核心组件包括testController、ActorManager、ChildActor和TestService,通过传递(Tracer, Span)元组在组件间传递追踪上下文。编写测试用例时,实现了MockTracer模拟Tracer实例,并用ScalaMock对TestServiceTrait进行打桩。运行测试时抛出org.scalatest.exceptions.TestFailedException,实际调用testMethod传入的Span与预期实例不匹配,异常信息如下:
org.scalatest.exceptions.TestFailedException: Unexpected call: <mock-1> TestServiceTrait.testMethod((io.opentelemetry.sdk.trace.SdkTracer@34252032,SdkSpan{traceId=bda0de82b71e2d40ab73ce47dd7bbc5d, spanId=b9cdbf86f8a2d49b, parentSpanContext=ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}, name=test-Route: Span, kind=INTERNAL, attributes=null, status=ImmutableStatusData{statusCode=UNSET, description=}, totalRecordedEvents=2, totalRecordedLinks=0, startEpochNanos=1726233760709644000, endEpochNanos=0})) Expected: inAnyOrder { <mock-1> TestServiceTrait.testMethod((io.opentelemetry.sdk.trace.SdkTracer@34252032,SdkSpan{traceId=67f3f71146238885eb25b1174cc7aa5b, spanId=2cd26dc2e079eabc, parentSpanContext=ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}, name=test ground truth Span, kind=INTERNAL, attributes=null, status=ImmutableStatusData{statusCode=UNSET, description=}, totalRecordedEvents=1, totalRecordedLinks=0, startEpochNanos=1726233760547292000, endEpochNanos=0})) any number of times (never called) } Actual: <mock-1> TestServiceTrait.testMethod((io.opentelemetry.sdk.trace.SdkTracer@34252032,SdkSpan{traceId=bda0de82b71e2d40ab73ce47dd7bbc5d, spanId=b9cdbf86f8a2d49b, parentSpanContext=ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}, name=test-Route: Span, kind=INTERNAL, attributes=null, status=ImmutableStatusData{statusCode=UNSET, description=}, totalRecordedEvents=2, totalRecordedLinks=0, startEpochNanos=1726233760709644000, endEpochNanos=1726233760718052915})) at org.scalamock.scalatest.AbstractMockFactory.newExpectationException(AbstractMockFactory.scala:51) at org.scalamock.scalatest.AbstractMockFactory.newExpectationException$(AbstractMockFactory.scala:50) at trainingtests.drivinglicense.TestClass.newExpectationException(TestClass.scala:31) at trainingtests.drivinglicense.TestClass.newExpectationException(TestClass.scala:31) at org.scalamock.context.MockContext.reportUnexpectedCall(MockContext.scala:42) at org.scalamock.context.MockContext.reportUnexpectedCall$(MockContext.scala:41) at trainingtests.drivinglicense.TestClass.reportUnexpectedCall(TestClass.scala:31) at org.scalamock.function.MockFunction.onUnexpected(MockFunction.scala:30) at org.scalamock.function.MockFunction.onUnexpected$(MockFunction.scala:30) at org.scalamock.function.MockFunction1.onUnexpected(MockFunction.scala:43) at org.scalamock.function.MockFunction1.onUnexpected(MockFunction.scala:43) at org.scalamock.function.FakeFunction.$anonfun$handle$1(FakeFunction.scala:44) at scala.Option.getOrElse(Option.scala:201) at org.scalamock.function.FakeFunction.handle(FakeFunction.scala:44) at org.scalamock.function.FakeFunction1.apply(FakeFunction.scala:63) at trainingtests.drivinglicense.TestClass$$anon$1.testMethod(TestClass.scala:56) at training.trainingmodels.drivinglicenses.actors.worker.ChildActor$$anonfun$receive$1.applyOrElse(ChildActor.scala:38) at org.apache.pekko.actor.Actor.aroundReceive(Actor.scala:547)
原因分析
- Span实例动态生成:测试中预期的是手动创建的
test ground truth Span,但实际执行时上游组件(如testController)动态创建了新的Span(test-Route: Span),导致传递的Span实例与预期完全不同。 - 引用匹配的局限性:ScalaMock默认使用实例引用相等性做匹配,而OpenTelemetry的Span每次运行都会生成新实例(含不同traceId、spanId),直接用预期实例匹配必然失败。
- Actor异步传递的上下文偏差:Pekko Actor的异步处理逻辑中,ActorManager可能在转发消息时重新创建了Span,而非传递测试用例中预先定义的Span实例。
解决方案
方案1:使用参数匹配器替代实例引用匹配
放弃直接匹配Span实例,改用ScalaMock的参数匹配器验证关键属性:
// 验证传入的Span属于目标Tracer且名称符合预期 (testService.testMethod _) expects (*, where { span: Span => span.getTracer == mockTracer && span.getName == "test-Route: Span" })
方案2:复用测试预定义Span
修改测试环境的Span创建逻辑,让上游组件使用测试用例中预先创建的Span:
- 在测试用例中创建并保存
test ground truth Span实例。 - 注入
testController时,覆盖其Span创建逻辑,直接返回预定义的测试Span。 - 确保
ActorManager和ChildActor传递的是该预定义Span实例。
方案3:使用OpenTelemetry测试工具包
利用OpenTelemetry官方测试工具创建可控制的Tracer和Span:
import io.opentelemetry.sdk.testing.trace.TestTracerProvider // 创建测试专用TracerProvider val testTracerProvider = TestTracerProvider.create() val testTracer = testTracerProvider.get("test-tracer") // 创建测试Span并在逻辑中复用 val testSpan = testTracer.spanBuilder("test ground truth Span").startSpan() // 执行测试逻辑后,验证Span的传递和使用情况
方案4:验证Span核心属性
如果不需要严格匹配实例,可验证Span的关键标识属性:
val expectedTraceId = testSpan.getSpanContext.getTraceId (testService.testMethod _) expects (*, where { span: Span => span.getSpanContext.getTraceId == expectedTraceId && span.getName == "test ground truth Span" })
内容的提问来源于stack exchange,提问作者sarah w
相关产品推荐
相关产品推荐

