Skip to content

Commit fe6b741

Browse files
committed
feat(doc): Add JUnit testing documentation
1 parent 1d931de commit fe6b741

File tree

1 file changed

+350
-0
lines changed

1 file changed

+350
-0
lines changed

docs/how_to_test_with_junit.md

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
# How to Test With JUnit Guide
2+
3+
This guide covers the JUnit 5 testing utilities for writing instrumentation and unit tests.
4+
5+
## Config injection with `@WithConfig`
6+
7+
`@WithConfig` declares configuration overrides for tests. It injects system properties (`dd.` prefix) or environment variables (`DD_` prefix) and rebuilds the `Config` singleton before each test.
8+
9+
### Class-level config
10+
11+
Applies to all tests in the class:
12+
13+
```java
14+
@WithConfig(key = "service", value = "my-service")
15+
@WithConfig(key = "trace.analytics.enabled", value = "true")
16+
class MyTest extends DDJavaSpecification {
17+
@Test
18+
void test() {
19+
// dd.service=my-service and dd.trace.analytics.enabled=true are set
20+
}
21+
}
22+
```
23+
24+
### Method-level config
25+
26+
Applies to a single test method, in addition to class-level config:
27+
28+
```java
29+
@WithConfig(key = "service", value = "my-service")
30+
class MyTest extends DDJavaSpecification {
31+
@Test
32+
@WithConfig(key = "trace.resolver.enabled", value = "false")
33+
void testWithResolverDisabled() {
34+
// dd.service=my-service AND dd.trace.resolver.enabled=false
35+
}
36+
37+
@Test
38+
void testWithDefaults() {
39+
// only dd.service=my-service
40+
}
41+
}
42+
```
43+
44+
### Environment variables
45+
46+
Use `env = true` to set an environment variable instead of a system property:
47+
48+
```java
49+
@WithConfig(key = "AGENT_HOST", value = "localhost", env = true)
50+
```
51+
52+
### Raw keys (no auto-prefix)
53+
54+
Use `addPrefix = false` to skip the automatic `dd.`/`DD_` prefix:
55+
56+
```java
57+
@WithConfig(key = "OTEL_SERVICE_NAME", value = "test", env = true, addPrefix = false)
58+
```
59+
60+
### Config with constant references
61+
62+
Annotation values accept compile-time constants:
63+
64+
```java
65+
@WithConfig(key = TracerConfig.TRACE_RESOLVER_ENABLED, value = "false")
66+
```
67+
68+
### Inheritance
69+
70+
`@WithConfig` on a superclass applies to all subclasses. When a subclass adds its own `@WithConfig`, both the parent's and the subclass's configs are applied (parent first, subclass second). This allows base classes to set shared config while subclasses add specifics:
71+
72+
```java
73+
@WithConfig(key = "integration.opentelemetry.experimental.enabled", value = "true")
74+
abstract class AbstractOtelTest extends AbstractInstrumentationTest { }
75+
76+
@WithConfig(key = "trace.propagation.style", value = "b3multi")
77+
class B3MultiTest extends AbstractOtelTest {
78+
// Both configs are active
79+
}
80+
```
81+
82+
### Composed annotations
83+
84+
Bundle multiple configs into a reusable annotation:
85+
86+
```java
87+
@Retention(RetentionPolicy.RUNTIME)
88+
@Target({ElementType.TYPE, ElementType.METHOD})
89+
@WithConfig(key = "iast.enabled", value = "true")
90+
@WithConfig(key = "iast.detection.mode", value = "FULL")
91+
@WithConfig(key = "iast.redaction.enabled", value = "false")
92+
public @interface IastFullDetection {}
93+
```
94+
95+
Then reuse across test classes:
96+
97+
```java
98+
@IastFullDetection
99+
class IastTagTest extends DDJavaSpecification { }
100+
101+
@IastFullDetection
102+
class IastReporterTest extends DDJavaSpecification { }
103+
```
104+
105+
### Imperative config injection
106+
107+
For dynamic values that can't be expressed in annotations, use the static methods directly:
108+
109+
```java
110+
@Test
111+
void testDynamic() {
112+
String port = startServer();
113+
WithConfigExtension.injectSysConfig("trace.agent.port", port);
114+
// ...
115+
}
116+
```
117+
118+
### Lifecycle
119+
120+
Config is rebuilt from a clean slate before each test:
121+
122+
1. **`beforeAll`**: class-level `@WithConfig` applied + config rebuilt (available for `@BeforeAll` methods)
123+
2. **`beforeEach`**: properties restored, class + method `@WithConfig` applied, config rebuilt
124+
3. **`afterEach`**: env vars cleared, properties restored, config rebuilt
125+
126+
This means each test starts with a clean config, and method-level `@WithConfig` doesn't leak between tests.
127+
128+
## Instrumentation tests with `AbstractInstrumentationTest`
129+
130+
`AbstractInstrumentationTest` is the JUnit 5 base class for instrumentation tests. It installs the agent once per test class, creates a shared tracer and writer, and provides trace assertion helpers.
131+
132+
### Lifecycle
133+
134+
| Phase | Scope | What happens |
135+
|---|---|---|
136+
| `@BeforeAll initAll()` | Once per class | Creates tracer + writer, installs ByteBuddy agent |
137+
| `@BeforeEach init()` | Per test | Flushes tracer, resets writer |
138+
| `@AfterEach tearDown()` | Per test | Flushes tracer |
139+
| `@AfterAll tearDownAll()` | Once per class | Closes tracer, removes agent transformer |
140+
141+
### Available fields
142+
143+
- `tracer` — the DD `TracerAPI` instance (shared across tests in the class)
144+
- `writer` — the `ListWriter` that captures traces written by the tracer
145+
146+
### Configuring the tracer
147+
148+
The tracer can be configured at class level using the `testConfig` builder. Call it from a static initializer (runs before `@BeforeAll`):
149+
150+
```java
151+
class MyTest extends AbstractInstrumentationTest {
152+
static {
153+
testConfig.idGenerationStrategy("RANDOM").strictTraceWrites(false);
154+
}
155+
}
156+
```
157+
158+
Available settings:
159+
160+
| Method | Default | Description |
161+
|---|---|---|
162+
| `idGenerationStrategy(String)` | `"SEQUENTIAL"` | Span ID generation strategy |
163+
| `strictTraceWrites(boolean)` | `true` | Enable strict trace write validation |
164+
165+
### Basic test
166+
167+
```java
168+
class HttpInstrumentationTest extends AbstractInstrumentationTest {
169+
@Test
170+
void testHttpRequest() {
171+
// exercise the instrumented code
172+
makeHttpRequest("http://example.com/api");
173+
174+
// assert the trace structure
175+
assertTraces(
176+
trace(
177+
span().root().operationName("http.request").resourceName("GET /api")));
178+
}
179+
}
180+
```
181+
182+
## Trace assertion API
183+
184+
The assertion API verifies trace structure using a fluent builder pattern. Import the static factories:
185+
186+
```java
187+
import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
188+
import static datadog.trace.agent.test.assertions.SpanMatcher.span;
189+
import static datadog.trace.agent.test.assertions.TagsMatcher.*;
190+
import static datadog.trace.agent.test.assertions.Matchers.*;
191+
```
192+
193+
### Asserting traces
194+
195+
`assertTraces` waits for traces to arrive (20s timeout), then verifies the structure:
196+
197+
```java
198+
// Single trace with 2 spans
199+
assertTraces(
200+
trace(
201+
span().root().operationName("parent"),
202+
span().childOfPrevious().operationName("child")));
203+
204+
// Multiple traces
205+
assertTraces(
206+
trace(span().root().operationName("trace-1")),
207+
trace(span().root().operationName("trace-2")));
208+
```
209+
210+
### Trace options
211+
212+
```java
213+
import static datadog.trace.agent.test.assertions.TraceAssertions.*;
214+
215+
// Ignore extra traces beyond the expected ones
216+
assertTraces(IGNORE_ADDITIONAL_TRACES,
217+
trace(span().root().operationName("expected")));
218+
219+
// Sort traces by start time before assertion
220+
assertTraces(SORT_BY_START_TIME,
221+
trace(span().root().operationName("first")),
222+
trace(span().root().operationName("second")));
223+
```
224+
225+
### Span matching
226+
227+
```java
228+
span()
229+
// Identity
230+
.root() // root span (parent ID = 0)
231+
.childOfPrevious() // child of previous span in trace
232+
.childOf(parentSpanId) // child of specific span
233+
234+
// Properties
235+
.operationName("http.request") // exact match
236+
.operationName(Pattern.compile("http.*"))// regex match
237+
.resourceName("GET /api") // exact match
238+
.serviceName("my-service") // exact match
239+
.type("web") // span type
240+
241+
// Error
242+
.error() // expects error = true
243+
.error(false) // expects error = false
244+
245+
// Duration
246+
.durationShorterThan(Duration.ofMillis(100))
247+
.durationLongerThan(Duration.ofMillis(1))
248+
249+
// Tags
250+
.tags(
251+
defaultTags(), // all standard DD tags
252+
tag("http.method", is("GET")), // exact tag value
253+
tag("db.type", is("postgres")))
254+
255+
// Span links
256+
.links(
257+
SpanLinkMatcher.to(otherSpan),
258+
SpanLinkMatcher.any())
259+
```
260+
261+
### Tag matching
262+
263+
```java
264+
// Default DD tags (thread name, runtime ID, sampling, etc.)
265+
defaultTags()
266+
267+
// Exact value
268+
tag("http.status", is(200))
269+
270+
// Custom validation
271+
tag("response.body", validates(v -> ((String) v).contains("success")))
272+
273+
// Any value (just check presence)
274+
tag("custom.tag", any())
275+
276+
// Error tags from exception
277+
error(IOException.class)
278+
error(IOException.class, "Connection refused")
279+
error(new IOException("Connection refused"))
280+
281+
// Check tag presence without value check
282+
includes("tag1", "tag2")
283+
```
284+
285+
### Value matchers
286+
287+
```java
288+
is("expected") // equality
289+
isNull() // null check
290+
isNonNull() // non-null check
291+
isTrue() // boolean true
292+
isFalse() // boolean false
293+
matches("regex.*") // regex match
294+
matches(Pattern.compile("..."))
295+
validates(v -> ...) // custom predicate
296+
any() // accept anything
297+
```
298+
299+
### Span link matching
300+
301+
```java
302+
// Link to a specific span
303+
SpanLinkMatcher.to(parentSpan)
304+
305+
// Link with trace/span IDs
306+
SpanLinkMatcher.to(traceId, spanId)
307+
308+
// Link with additional properties
309+
SpanLinkMatcher.to(span)
310+
.traceFlags((byte) 0x01)
311+
.traceState("vendor=value")
312+
313+
// Accept any link
314+
SpanLinkMatcher.any()
315+
```
316+
317+
### Sorting spans within a trace
318+
319+
```java
320+
import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
321+
322+
assertTraces(
323+
trace(
324+
SORT_BY_START_TIME,
325+
span().root().operationName("parent"),
326+
span().childOfPrevious().operationName("child")));
327+
```
328+
329+
### Waiting for traces
330+
331+
```java
332+
// Wait until a condition is met (20s timeout)
333+
blockUntilTracesMatch(traces -> traces.size() >= 2);
334+
335+
// Wait for child spans to finish
336+
blockUntilChildSpansFinished(3);
337+
```
338+
339+
### Accessing traces directly
340+
341+
For assertions not covered by the fluent API, access the writer directly:
342+
343+
```java
344+
writer.waitForTraces(1);
345+
List<DDSpan> trace = writer.firstTrace();
346+
DDSpan span = trace.get(0);
347+
348+
assertEquals("expected-op", span.getOperationName().toString());
349+
assertEquals(42L, span.getTag("custom.metric"));
350+
```

0 commit comments

Comments
 (0)