This version is still in development and is not considered stable yet. For the latest stable version, please use Micrometer Tracing 1.5.3!

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

The Wavefront Reporter and its test support have been deprecated because Wavefront’s End of Life Announcement.

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

}