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

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)
原因分析
  1. Span实例动态生成:测试中预期的是手动创建的test ground truth Span,但实际执行时上游组件(如testController)动态创建了新的Span(test-Route: Span),导致传递的Span实例与预期完全不同。
  2. 引用匹配的局限性:ScalaMock默认使用实例引用相等性做匹配,而OpenTelemetry的Span每次运行都会生成新实例(含不同traceId、spanId),直接用预期实例匹配必然失败。
  3. 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:

  1. 在测试用例中创建并保存test ground truth Span实例。
  2. 注入testController时,覆盖其Span创建逻辑,直接返回预定义的测试Span。
  3. 确保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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 13:52:01