Skip to content

AsyncAPI 3.x: an E2E test, NCS driven through Kafka - #1810

Draft
LautaroPetaccio wants to merge 4 commits into
feature/asyncapi-test-writerfrom
feature/asyncapi-e2e-kafka
Draft

LautaroPetaccio wants to merge 4 commits into
feature/asyncapi-test-writerfrom
feature/asyncapi-e2e-kafka

Conversation

@LautaroPetaccio

Copy link
Copy Markdown
Collaborator

Last of the AsyncAPI 3.x series, on top of #1809. Replaces #1760, which could not be retargeted in place.

The SUT — NCS over Kafka

The NCS case study re-expressed as a message-driven service: six request topics (ncs.<op>.request), each answered on a reply topic with a result message or, for the inputs the REST version answers with a 400, an error message — exactly what ncs-kafka.yaml (already in core's test resources) describes. The routines are ported from EMB's org.restncs.imp and cited at the top of each file; the rejection rules mirror NcsRest. A Spring Boot application with no web layer: it speaks only Kafka, which is the point.

One deliberate deviation: a result that is NaN or infinite is answered as an error. JSON has no such numbers, and a string where the contract promises a number would be a false fault.

And one deliberate defect, marked as such in NcsService where it is written: a triangle with a non-positive edge is answered with {"classification": ...}, a shape the document declares nowhere for that reply. The original NCS routine classifies it as 0 and answers with the declared result. It is here because the reply classifier's fault needs a service that commits it: the message arrives, it correlates, nothing times out, and a client written against the contract still cannot read it. Nothing else in the run notices, and the generated suite is expected to report it. Worth knowing before reading the fixture as a faithful port — it is faithful everywhere else.

The driver — the reference executeAsyncApiAction

NcsKafkaController owns the broker (testcontainers, apache/kafka:3.8.0, KRaft), creates the twelve topics up front, and implements the hook exactly as #1738's javadoc sketches it:

  • publish with the correlation id in the header the document names — or stamped into the payload at the declared pointer, for documents that say so;
  • the reply consumer is assigned and positioned at the end before publishing, so a fast reply cannot slip past, and reused for the rest of the run;
  • wait until a record carrying the id arrives or replyTimeoutMs runs out; a reply carrying someone else's id is skipped, one carrying none is taken and reported as correlationMatched = false;
  • fill AsyncApiReplyDto and judge nothing.

It also answers getAsyncApiServerAddress, so a generated test reaches the broker this run actually started rather than the address the document declares.

kafka-clients and testcontainers-kafka appear only in this test module, managed in the root pom.xml as the doc requires. Nothing is added to any shipped jar.

The search

AsyncApiTestBase joins the other bases in e2e-tests-utils: initAndRun, and helpers that read AsyncApiCallResult off the solution. NcsKafkaEMTest runs 300 action evaluations and asserts:

  • every operation was answered;
  • checkTriangle, bessj and remainder reached their result reply;
  • expint and gammq reached both their result and their error — the two operations whose rejected inputs (x < 0; a ≤ 0 || x < 0) the schema does not rule out. bessj's order and remainder's operands are bounded in the schema, so their error replies need invalid data, which the search does not publish yet. Both documents now say so beside the reply, so the next reader does not take it for a gap;
  • the undeclared reply is reported against checkTriangle, and nothing is reported against the operations that answer correctly;
  • no NO_REPLY, no PUBLISH_FAILED.

A second search runs with the oracles on and a reply deadline no round trip can meet, so every message goes unanswered and the no-reply fault is reported. Neither side of that gate had been exercised against a live broker before.

Because the driver is embedded, the SUT is instrumented, so the search covers lines and branches of a message-driven service as well: 662 targets in 26 seconds, identical on two runs.

The generated suite

The run now also writes the suite, compiles it and runs it against the same broker, so #1809 is covered by the same E2E. Both output languages are generated: Kotlin in the first run, Java in a second one, because a Java half that never compiles is invisible to a Kotlin-only check. The shared compile step learned to tell .java sources from .kt ones to make that possible.

CI

The module starts a broker in a container, so it wants Docker and is slower than the rest. It gets an AsyncApiTests profile and a skipAsyncApi flag every other profile turns off, which is how the database E2Es are kept out of runs that have no business paying for them, and CI gains the matching job.

The first search against a real broker. The SUT is the NCS case study
re-expressed as a Kafka service: six request topics answered on six reply
topics with a result or an error, as ncs-kafka.yaml describes. It speaks
only Kafka; there is no HTTP endpoint. Its routines are ported from EMB's
NCS and cited as such.

The driver owns the broker, a testcontainers apache/kafka, and is the
reference implementation of SutController.executeAsyncApiAction: publish
with the correlation id where the document says it goes, wait for the
reply that carries it back, report and do not judge. kafka-clients and
testcontainers-kafka are used only here, in the test tree, and are managed
in the root pom like everything else.

The test asserts that every operation answers and that expint and gammq
each reach both their declared replies. Those two are the operations whose
rejected inputs the schema does not rule out; bessj's and remainder's
bounds are in the schema, so their errors need invalid data, which the
search does not publish yet. 662 targets in 26 seconds, on two runs.
Neither side of the experimental oracle gate was exercised against a live
broker. A second search now runs with the oracles on and a reply deadline no
round trip can meet, so every message goes unanswered and the fault is
reported, while the existing search asserts that a service which answers
correctly has nothing reported against it.

Nothing in the service is broken on purpose for this. The driver ignores
replies whose correlation id does not match the request it is waiting on, so
the late answers piling up on the reply topics cannot be taken for fresh ones.
The search was covered by a run against a real broker; what the run wrote
was not. Both output languages are now generated, compiled and executed
against the same broker: Kotlin in the existing run, Java in a second one,
because a Java half that never compiles is invisible to a Kotlin-only check.

The service gains one deliberate defect, marked as such where it is written:
a non-positive triangle edge is answered with a payload the document declares
nowhere for that reply. It correlates, it arrives, and a client written
against the contract still cannot read it, so it is the one fault only the
reply classifier finds. The suite is expected to report it.

The two replies whose errors no conforming client can reach say so in the
document, beside the reply itself, so the next reader does not take them for
a gap in the search.

The driver compared the correlation location against the two string constants
that have since become an enum, and the shared compile step assumed Kotlin
sources.
The module starts a broker in a container, so it wants Docker and it is
slower than the rest. It gets a profile of its own and a flag every other
profile turns off, which is how the database E2Es are kept out of the runs
that have no business paying for them, and CI gains the job that turns it on.
@LautaroPetaccio
LautaroPetaccio added this pull request to stack #1812 September 30, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant