For the latest stable version, please use Micrometer Tracing 1.4.0!

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")));
        };
    }

}