Skip to content

Commit c659547

Browse files
committed
feat(spring-boot): Migrate smoke test to JUnit
1 parent a611184 commit c659547

4 files changed

Lines changed: 201 additions & 120 deletions

File tree

dd-smoke-tests/spring-boot-rabbit/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626

2727
testImplementation project(':dd-smoke-tests')
2828
testImplementation group: 'org.testcontainers', name: 'rabbitmq', version: libs.versions.testcontainers.get()
29+
testImplementation group: 'org.testcontainers', name: 'junit-jupiter', version: libs.versions.testcontainers.get()
2930
}
3031

3132
tasks.withType(Test).configureEach {

dd-smoke-tests/spring-boot-rabbit/gradle.lockfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ org.apache.logging.log4j:log4j-to-slf4j:2.14.1=compileClasspath,runtimeClasspath
8080
org.apache.tomcat.embed:tomcat-embed-core:9.0.52=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
8181
org.apache.tomcat.embed:tomcat-embed-el:9.0.52=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
8282
org.apache.tomcat.embed:tomcat-embed-websocket:9.0.52=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
83-
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
83+
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testRuntimeClasspath
8484
org.codehaus.groovy:groovy-ant:3.0.23=codenarc
8585
org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc
8686
org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc
@@ -158,6 +158,7 @@ org.springframework:spring-web:5.3.9=compileClasspath,runtimeClasspath,testCompi
158158
org.springframework:spring-webmvc:5.3.9=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
159159
org.tabletest:tabletest-junit:1.2.1=testCompileClasspath,testRuntimeClasspath
160160
org.tabletest:tabletest-parser:1.2.0=testCompileClasspath,testRuntimeClasspath
161+
org.testcontainers:junit-jupiter:1.21.4=testCompileClasspath,testRuntimeClasspath
161162
org.testcontainers:rabbitmq:1.21.4=testCompileClasspath,testRuntimeClasspath
162163
org.testcontainers:testcontainers:1.21.4=testCompileClasspath,testRuntimeClasspath
163164
org.xmlresolver:xmlresolver:5.3.3=spotbugs

dd-smoke-tests/spring-boot-rabbit/src/test/groovy/datadog/smoketest/SpringBootRabbitIntegrationTest.groovy

Lines changed: 0 additions & 119 deletions
This file was deleted.
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package datadog.smoketest;
2+
3+
import static datadog.smoketest.trace.SpanMatcher.span;
4+
import static datadog.smoketest.trace.TraceMatcher.SORT_BY_ANCESTRY;
5+
import static datadog.smoketest.trace.TraceMatcher.trace;
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
8+
import datadog.smoketest.backend.EnabledIfDockerAvailable;
9+
import datadog.smoketest.backend.TestAgentBackend;
10+
import datadog.smoketest.backend.TraceBackend;
11+
import datadog.smoketest.backend.Traces;
12+
import datadog.smoketest.trace.SmokeTraceAssertions;
13+
import datadog.smoketest.trace.SpanMatcher;
14+
import datadog.smoketest.trace.TraceMatcher;
15+
import datadog.trace.test.agent.decoder.DecodedSpan;
16+
import java.io.IOException;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import okhttp3.OkHttpClient;
20+
import okhttp3.Request;
21+
import okhttp3.Response;
22+
import org.junit.jupiter.api.Order;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.RegisterExtension;
25+
import org.testcontainers.containers.RabbitMQContainer;
26+
import org.testcontainers.junit.jupiter.Container;
27+
import org.testcontainers.junit.jupiter.Testcontainers;
28+
import org.testcontainers.utility.DockerImageName;
29+
30+
/**
31+
* S8 multi-app pilot — ported from the Groovy {@code SpringBootRabbitIntegrationTest}. Two Spring
32+
* Boot apps (a sender and a receiver) round-trip a message through RabbitMQ and both report to one
33+
* <em>shared</em> test-agent backend (Q8), so a single {@code @RegisterExtension} agent captures
34+
* the distributed traces from both JVMs.
35+
*
36+
* <p>Replaces the dropped {@code TraceStructureWriter} file-mode (Q10) with {@link DecodedSpan}
37+
* assertions, and <em>hardens</em> the Groovy {@code expectedTraces}. With {@code
38+
* dd.rabbit.legacy.tracing.enabled=false} the whole round-trip is a single context-propagated trace
39+
* spanning both JVMs and the broker — a strict parent→child chain of 12 spans, rooted at the sender
40+
* {@code servlet.request}:
41+
*
42+
* <pre>
43+
* spring-rabbit-0 servlet.request GET /roundtrip/{message}
44+
* spring-rabbit-0 spring.handler WebController.roundtrip
45+
* spring-rabbit-0 amqp.command basic.publish -> otherqueue (send)
46+
* rabbitmq amqp.deliver amqp.deliver otherqueue
47+
* spring-rabbit-1 amqp.command basic.deliver otherqueue
48+
* spring-rabbit-1 amqp.consume amqp.consume otherqueue
49+
* spring-rabbit-1 spring.consume Receiver.receiveMessage (receiver consumes)
50+
* spring-rabbit-1 amqp.command basic.publish -> queue (receiver forwards reply)
51+
* rabbitmq amqp.deliver amqp.deliver queue
52+
* spring-rabbit-0 amqp.command basic.deliver queue
53+
* spring-rabbit-0 amqp.consume amqp.consume queue
54+
* spring-rabbit-0 spring.consume Receiver.receiveMessage (sender consumes reply)
55+
* </pre>
56+
*
57+
* <p>The whole collection is asserted with the standard {@link Traces#assertTraces} DSL in
58+
* {@linkplain SmokeTraceAssertions order-independent subset} mode ({@code
59+
* unordered().ignoreAdditionalTraces()}): each matcher matches a distinct received trace and extras
60+
* are ignored. This is stronger than the Groovy set-membership check — each round-trip trace is
61+
* matched count-exact (all 12 spans, in {@link TraceMatcher#SORT_BY_ANCESTRY ancestry order}),
62+
* verifying every AMQP operation (publish/deliver/consume, both directions) <em>and</em> its
63+
* cross-service linkage — while staying robust to the timing-dependent extras (the broker emits its
64+
* connection-setup commands and per-ack traces as their own single-span traces, in
65+
* non-deterministic count and order).
66+
*
67+
* <p>Two design points, informed by inspecting the live trace collection:
68+
*
69+
* <ul>
70+
* <li><b>Ancestry order, not start time</b> — the 12-span round-trip is a strict linear chain,
71+
* but its spans start within the same tick and race, so {@code SORT_BY_START_TIME} is
72+
* unstable across runs. {@code SORT_BY_ANCESTRY} orders each parent before its child
73+
* (timestamp-independent along the chain), giving a stable positional order.
74+
* <li><b>Accumulate, don't isolate</b> — {@code .retainAcrossTests()} keeps traces from app
75+
* startup onward, because {@code basic.qos}/{@code basic.consume}/{@code queue.declare} are
76+
* emitted when the consumers start (before any test method), which a per-method session
77+
* {@code clear()} would discard. Mirrors the Groovy base verifying once at {@code
78+
* cleanupSpec} against everything since launch.
79+
* </ul>
80+
*/
81+
@EnabledIfDockerAvailable
82+
@Testcontainers
83+
class SpringBootRabbitSmokeTest {
84+
private static final int TIMEOUT_SECONDS = 60;
85+
private static final int RABBIT_AMQP_PORT = 5672;
86+
private static final OkHttpClient CLIENT = new OkHttpClient();
87+
// AMQP connection-setup / ack commands each app emits as its own (single-span) trace.
88+
private static final String[] ADMIN_COMMANDS = {
89+
"basic.qos", "basic.consume", "basic.ack", "queue.declare"
90+
};
91+
92+
@Container
93+
private static final RabbitMQContainer RABBIT =
94+
new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.9.20-alpine"));
95+
96+
@Order(1)
97+
@RegisterExtension
98+
static final TestAgentBackend agent =
99+
TraceBackend.testAgentBuilder().shared().retainAcrossTests().build();
100+
101+
@Order(2)
102+
@RegisterExtension
103+
static final SmokeServerApp sender =
104+
rabbitApp(0).args("--rabbit.sender.queue=otherqueue").build();
105+
106+
@Order(3)
107+
@RegisterExtension
108+
static final SmokeServerApp receiver =
109+
rabbitApp(1)
110+
.args("--rabbit.receiver.queue=otherqueue", "--rabbit.receiver.forward=true")
111+
.build();
112+
113+
@Test
114+
void roundtripsProduceFullAmqpTraceStructure() throws IOException {
115+
// Drive 3 round-trips through the sender;
116+
// Each travels sender -> otherqueue -> receiver -> queue -> sender.
117+
String[] MESSAGES = {"foo", "bar", "baz"};
118+
for (String message : MESSAGES) {
119+
Request request =
120+
new Request.Builder().url(sender.url() + "/roundtrip/" + message).get().build();
121+
try (Response response = CLIENT.newCall(request).execute()) {
122+
assertEquals(200, response.code(), "roundtrip " + message);
123+
assertEquals("Got: >" + message, response.body().string(), "roundtrip " + message);
124+
}
125+
}
126+
127+
// One order-independent subset assertion over the whole collection (unordered +
128+
// ignoreAdditionalTraces): each matcher must match a distinct received trace, and
129+
// unrelated/duplicate traces (extra acks, etc.) are ignored. Assert one full round-trip trace
130+
// per message plus each service's connection-setup/ack commands.
131+
List<TraceMatcher> expected = new ArrayList<>();
132+
for (int i = 0; i < MESSAGES.length; i++) {
133+
expected.add(roundTrip());
134+
}
135+
for (String service : new String[] {"spring-rabbit-0", "spring-rabbit-1"}) {
136+
for (String command : ADMIN_COMMANDS) {
137+
expected.add(admin(service, command));
138+
}
139+
}
140+
agent
141+
.traces()
142+
.assertTraces(
143+
TIMEOUT_SECONDS,
144+
o -> o.unorder().ignoreAdditionalTraces(),
145+
expected.toArray(new TraceMatcher[0]));
146+
}
147+
148+
// The full distributed round-trip as one strict parent->child chain across both services and the
149+
// broker. SORT_BY_ANCESTRY orders the spans parent->child (stable for a linear chain), and each
150+
// matcher after the root pins its parent to the preceding span with childOfPrevious() — so the 12
151+
// count-exact positional matchers assert both the shape and the explicit parent linkage: HTTP
152+
// entrypoint -> publish -> receiver consumes+forwards -> sender consumes reply.
153+
private static TraceMatcher roundTrip() {
154+
return trace(
155+
SORT_BY_ANCESTRY,
156+
sp("spring-rabbit-0", "servlet.request", "GET /roundtrip/{message}").root(),
157+
sp("spring-rabbit-0", "spring.handler", "WebController.roundtrip").childOfPrevious(),
158+
sp("spring-rabbit-0", "amqp.command", "basic.publish <default> -> otherqueue")
159+
.childOfPrevious(),
160+
sp("rabbitmq", "amqp.deliver", "amqp.deliver otherqueue").childOfPrevious(),
161+
sp("spring-rabbit-1", "amqp.command", "basic.deliver otherqueue").childOfPrevious(),
162+
sp("spring-rabbit-1", "amqp.consume", "amqp.consume otherqueue").childOfPrevious(),
163+
sp("spring-rabbit-1", "spring.consume", "Receiver.receiveMessage").childOfPrevious(),
164+
sp("spring-rabbit-1", "amqp.command", "basic.publish <default> -> queue").childOfPrevious(),
165+
sp("rabbitmq", "amqp.deliver", "amqp.deliver queue").childOfPrevious(),
166+
sp("spring-rabbit-0", "amqp.command", "basic.deliver queue").childOfPrevious(),
167+
sp("spring-rabbit-0", "amqp.consume", "amqp.consume queue").childOfPrevious(),
168+
sp("spring-rabbit-0", "spring.consume", "Receiver.receiveMessage").childOfPrevious());
169+
}
170+
171+
// A connection-setup / ack command emitted as its own single-span (root) trace.
172+
private static TraceMatcher admin(String service, String command) {
173+
return trace(sp(service, "amqp.command", command).root());
174+
}
175+
176+
private static SpanMatcher sp(String service, String operation, String resource) {
177+
return span().service(service).operationName(operation).resourceName(resource);
178+
}
179+
180+
private static SmokeServerApp.Builder rabbitApp(int index) {
181+
return SmokeServerApp.named("spring-rabbit-" + index)
182+
.jar(System.getProperty("datadog.smoketest.springboot.shadowJar.path"))
183+
.backend(agent)
184+
.jvmArgs(
185+
"-Ddd.service.name=spring-rabbit-" + index, "-Ddd.rabbit.legacy.tracing.enabled=false")
186+
// Resolved at launch, after @Testcontainers has started RABBIT — not at build time.
187+
.placeholder("rabbit.host", RABBIT::getHost)
188+
.placeholder("rabbit.port", () -> String.valueOf(RABBIT.getMappedPort(RABBIT_AMQP_PORT)))
189+
.args(
190+
"--server.port=${app.httpPort}",
191+
"--spring.rabbitmq.host=${rabbit.host}",
192+
"--spring.rabbitmq.port=${rabbit.port}")
193+
// The broker connection is torn down noisily when the app is killed at teardown.
194+
.allowedErrorLogs(
195+
"Failed to check/redeclare auto-delete queue(s)",
196+
"An unexpected connection driver error occurred");
197+
}
198+
}

0 commit comments

Comments
 (0)