Testing
Micrometer Tracing includes the micrometer-tracing-test
and micrometer-tracing-integration-test
modules.
For unit tests, it provides a SimpleTracer
that is a test implementation of a Tracer
.
For integration tests, it provides a SampleTestRunner
mechanism that you can hook into your samples.
It:
-
Configures an OpenZipkin Brave Tracer
-
Sets it up with Tanzu Observability by Wavefront Reporter
-
Sets it up with OpenZipkin Zipkin Reporter
-
-
Configures an OpenTelemetry Tracer
-
Sets it up with Tanzu Observability by Wavefront Exporter
-
Sets it up with OpenZipkin Zipkin Exporter
-
-
Runs all the combinations above against the user code and running infrastructure
Installing
The following example shows the required dependency in Gradle (assuming that Micrometer Tracing BOM has been added):
testImplementation 'io.micrometer:micrometer-tracing-test' // for unit tests
testImplementation 'io.micrometer:micrometer-tracing-integration-test' // for integration tests
The following example shows the required dependency in Maven (assuming that Micrometer Tracing BOM has been added):
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-test</artifactId> <!-- For unit tests -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-integration-test</artifactId> <!-- For integration tests -->
<scope>test</scope>
</dependency>
Running Tracing Unit Tests
To run unit tests of your custom handler, you may want to use the SimpleTracer
test Tracer
implementation. Let’s assume the following custom TracingObservationHandler
:
static class MyTracingObservationHandler implements TracingObservationHandler<CustomContext> {
private final Tracer tracer;
MyTracingObservationHandler(Tracer tracer) {
this.tracer = tracer;
}
@Override
public void onStart(CustomContext context) {
String databaseName = context.getDatabaseName();
Span.Builder builder = this.tracer.spanBuilder().kind(Span.Kind.CLIENT).remoteServiceName(databaseName);
getTracingContext(context).setSpan(builder.start());
}
@Override
public void onError(CustomContext context) {
getTracingContext(context).getSpan().error(context.getError());
}
@Override
public void onStop(CustomContext context) {
Span span = getRequiredSpan(context);
span.name(context.getContextualName() != null ? context.getContextualName() : context.getName());
tagSpan(context, span);
span.end();
}
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof CustomContext;
}
@Override
public Tracer getTracer() {
return this.tracer;
}
}
To verify whether the spans got properly created we can use the SimpleTracer
, as follows:
class SomeComponentThatIsUsingMyTracingObservationHandlerTests {
ObservationRegistry registry = ObservationRegistry.create();
SomeComponent someComponent = new SomeComponent(registry);
SimpleTracer simpleTracer = new SimpleTracer();
MyTracingObservationHandler handler = new MyTracingObservationHandler(simpleTracer);
@BeforeEach
void setup() {
registry.observationConfig().observationHandler(handler);
}
@Test
void should_store_a_span() {
// this code will call actual Observation API
someComponent.doSthThatShouldCreateSpans();
TracerAssert.assertThat(simpleTracer)
.onlySpan()
.hasNameEqualTo("insert user")
.hasKindEqualTo(Span.Kind.CLIENT)
.hasRemoteServiceNameEqualTo("mongodb-database")
.hasTag("mongodb.command", "insert")
.hasTag("mongodb.collection", "user")
.hasTagWithKey("mongodb.cluster_id")
.assertThatThrowable()
.isInstanceOf(IllegalStateException.class)
.backToSpan()
.hasIpThatIsBlank()
.hasPortThatIsNotSet();
}
}
Running integration tests
The following example shows how you can run your code to test your integrations:
-
By asserting spans that were stored without emitting them to a reporting system
-
Against running Tanzu Observability by Wavefront instance (this option turns on when you have passed the Wavefront related configuration in the constructor - otherwise the test will be disabled)
-
Against running Zipkin instance (this option turns on when Zipkin is running - otherwise the test will be disabled)
class ObservabilitySmokeTest extends SampleTestRunner {
ObservabilitySmokeTest() {
super(SampleRunnerConfig.builder().wavefrontApplicationName("my-app").wavefrontServiceName("my-service")
.wavefrontToken("...")
.wavefrontUrl("...")
.zipkinUrl("...") // defaults to localhost:9411
.build());
}
@Override
public BiConsumer<BuildingBlocks, Deque<ObservationHandler<? extends Observation.Context>>> customizeObservationHandlers() {
return (bb, handlers) -> {
ObservationHandler defaultHandler = handlers.removeLast();
handlers.addLast(new MyTracingObservationHandler(bb.getTracer()));
handlers.addLast(defaultHandler);
};
}
@Override
public SampleTestRunnerConsumer yourCode() {
return (bb, meterRegistry) -> {
// here you would be running your code
yourCode();
SpansAssert.assertThat(bb.getFinishedSpans())
.haveSameTraceId()
.hasNumberOfSpansEqualTo(8)
.hasNumberOfSpansWithNameEqualTo("handle", 4)
.forAllSpansWithNameEqualTo("handle", span -> span.hasTagWithKey("rsocket.request-type"))
.hasASpanWithNameIgnoreCase("request_stream")
.thenASpanWithNameEqualToIgnoreCase("request_stream")
.hasTag("rsocket.request-type", "REQUEST_STREAM")
.backToSpans()
.hasASpanWithNameIgnoreCase("request_channel")
.thenASpanWithNameEqualToIgnoreCase("request_channel")
.hasTag("rsocket.request-type", "REQUEST_CHANNEL")
.backToSpans()
.hasASpanWithNameIgnoreCase("request_fnf")
.thenASpanWithNameEqualToIgnoreCase("request_fnf")
.hasTag("rsocket.request-type", "REQUEST_FNF")
.backToSpans()
.hasASpanWithNameIgnoreCase("request_response")
.thenASpanWithNameEqualToIgnoreCase("request_response")
.hasTag("rsocket.request-type", "REQUEST_RESPONSE");
MeterRegistryAssert.assertThat(meterRegistry)
.hasTimerWithNameAndTags("rsocket.response", Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE")))
.hasTimerWithNameAndTags("rsocket.fnf", Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_FNF")))
.hasTimerWithNameAndTags("rsocket.request", Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_RESPONSE")))
.hasTimerWithNameAndTags("rsocket.channel", Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_CHANNEL")))
.hasTimerWithNameAndTags("rsocket.stream", Tags.of(Tag.of("error", "none"), Tag.of("rsocket.request-type", "REQUEST_STREAM")));
};
}
}