Testing Your Observability
Part 4 of an 8-part series on implementing observability in Java microservices
You write unit tests for your business logic. You write integration tests for your database. Do you test your Observability?
If a tree falls in a forest and no one traces it, did it make a sound? If your payment service crashes but the error=true attribute wasn’t set on the span, your on-call engineer will be flying blind.
In this module, we’ll treat Instrumentation as Code and test it.
The Testing SDK
OpenTelemetry provides a lightweight in-memory SDK specifically for testing.
1. Add Dependencies
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-testing</artifactId>
<version>1.34.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
2. Setting up the Test Setup
We use JUnit 5 Extensions to spin up a real OTel SDK instance that writes to a List<SpanData> in memory instead of Jaeger.
import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
import org.junit.jupiter.api.RegisterExtension;
import org.junit.jupiter.api.Test;
class OrderServiceTest {
@RegisterExtension
static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
// Inject the Tracer from our Test SDK
private final OrderService orderService = new OrderService(otelTesting.getOpenTelemetry().getTracer("test"));
@Test
void processOrder_ShouldCreateCorrectSpans() {
// execute business logic
orderService.processOrder("user-123", new BigDecimal("99.00"));
// Assertions
List<SpanData> spans = otelTesting.getSpans();
// 1. Check a span was created
assertEquals(1, spans.size());
SpanData span = spans.get(0);
// 2. Check Span Name
assertEquals("process-order", span.getName());
// 3. Check Critical Attributes
assertEquals("user-123", span.getAttributes().get(AttributeKey.stringKey("order.customer_id")));
assertEquals(99.00, span.getAttributes().get(AttributeKey.doubleKey("order.amount")));
// 4. Verify no errors
assertEquals(StatusCode.UNSET, span.getStatus().getStatusCode());
}
}
Testing Error Scenarios
The most critical thing to test is failure visibility.
@Test
void processOrder_OnError_ShouldRecordException() {
// Force an error in logic
assertThrows(InvalidOrderException.class, () -> {
orderService.processOrder(null, null);
});
List<SpanData> spans = otelTesting.getSpans();
SpanData span = spans.get(0);
// Assert that the span is marked as ERROR
assertEquals(StatusCode.ERROR, span.getStatus().getStatusCode());
// Assert that the exception event was recorded
boolean hasExceptionEvent = span.getEvents().stream()
.anyMatch(event -> event.getName().equals("exception"));
assertTrue(hasExceptionEvent, "Span should record exception event");
}
Integration Testing (Spring Boot)
For integration tests where you spin up the full context, you can define a @TestConfiguration bean:
@TestConfiguration
public class ObservabilityTestConfig {
@Bean
public OpenTelemetry openTelemetry() {
// Returns a testing implementation that prevents OTel
// from trying to export to localhost:4317 during CI/CD
return OpenTelemetry.noop();
}
}
Tips for Testing
- Wait for Spans: The OTel SDK is asynchronous. In tests, use a short
sleepor an await library (e.g., Awaitility) to ensure spans have been flushed to the in-memory exporter. - Context Propagation: Verify that the
Trace IDis consistent across child spans to ensure your context propagation (Baggage/ParentContext) is working as expected.
Summary
Treat tracing like any other feature. If it’s critical for debugging (and it is), it deserves a test.