Covers replacing Thread.sleep with await() across a void @Async method, an @EmbeddedKafka-backed @KafkaListener, and an already-running @Scheduled job; verifies Awaitility 4.3.0's real defaults (10s timeout, 100ms poll interval) and its default-uncaught-exception-handler swap directly against the jar; and documents a real pom.xml trap where Boot 4.1.1 split Kafka's autoconfiguration (spring-boot-kafka) out of spring-kafka itself, which silently leaves @KafkaListener beans with no running container.
5.3 KiB
awaitility
Companion code for the Awaitility article on ankurm.com. All intermediate
and reference-grade depth lives in the post itself (accordions / "going deeper" paragraphs), not
in a docs/NN-topic.md chapter folder — the one exception, as in the rest of this repository, is
docs/output/, which holds real captured transcripts and nothing else.
What this is
Six real, runnable demonstrations of the same idea: a fixed Thread.sleep(...) in a test is a
guess about timing the test does not control, and Awaitility.await() replaces the guess with a
bounded poll loop. Each demonstration targets a different source of asynchrony:
| Test | What it waits for |
|---|---|
AwaitAsyncConfirmationTest |
A void @Async method with no Future to block on |
DefaultTimingTest |
Nothing — it proves Awaitility's own defaults (10s timeout, 100ms poll interval) against the real jar |
KafkaListenerAwaitTest |
An @KafkaListener consuming a record a KafkaTemplate just sent, against a real in-process @EmbeddedKafka broker |
ScheduledJobAwaitTest |
Three more executions of an already-running @Scheduled(fixedRate = 150) job |
UncaughtExceptionHandlerSwapTest |
Nothing — it proves await() temporarily installs its own Thread.setDefaultUncaughtExceptionHandler and restores the original afterward |
IgnoreExceptionsTest |
A resource that throws for its first 300ms, using ignoreExceptionsInstanceOf(...) to treat that as "not yet", not a failure |
Two more transcripts in docs/output/ are real failures from tests that are not in the suite
above: 01-sleep-guesses-wrong.txt (a fixed Thread.sleep(100) against a 220ms operation) and
08-exception-propagates-immediately.txt (the same resource as IgnoreExceptionsTest, minus
ignoreExceptionsInstanceOf(...)). Both were real mvn test runs, captured once, then the
failing test class was deleted — the mistake is preserved as a transcript, not as a permanently
red test.
The pom.xml trap this module exists to document
The first version of this module's pom.xml depended on org.springframework.kafka:spring-kafka
directly, the way every pre-Boot-4.1 tutorial does. It compiled. Every @SpringBootTest using
Kafka then failed with an empty ConcurrentLinkedQueue and no error at context startup, because
in Boot 4.1.1 the Kafka autoconfiguration classes (KafkaTemplate, ConsumerFactory, the
KafkaListenerEndpointRegistry that @KafkaListener needs) moved out of the monolithic
spring-boot-autoconfigure jar into their own module, org.springframework.boot:spring-boot-kafka
— pulled in by the new org.springframework.boot:spring-boot-starter-kafka, not by spring-kafka
alone. See pom.xml's own comments and the post for the full diagnosis.
Versions
Read from spring-boot-dependencies-4.1.1.pom and spring-boot-starter-test-4.1.1.pom on Maven
Central, not from release notes: JDK 25 (Temurin 25.0.4.1+1), Spring Boot 4.1.1,
Spring Kafka 4.1.1, Awaitility 4.3.0 (already on the classpath via
spring-boot-starter-test — no explicit <dependency> for it anywhere in pom.xml).
Quickstart
mvn test
11 tests, 0 failures. KafkaListenerAwaitTest starts a real in-process Kafka broker
(@EmbeddedKafka) and takes a few seconds; DefaultTimingTest deliberately waits out Awaitility's
real 10-second default timeout once, so the suite as a whole takes about 30 seconds.
Captured output
| File | What it's from |
|---|---|
00-full-test-run.txt |
The full mvn test run, 11/11 green |
01-sleep-guesses-wrong.txt |
Real failure: a guessed Thread.sleep(100) against a 220ms operation (test since removed) |
02-await-finds-it.txt |
The fixed version: await().untilAsserted(...) against the same operation |
03-default-timeout-is-ten-seconds.txt |
await().until(() -> false) timing out at ~10,000ms with no override |
04-default-poll-interval-is-100ms.txt |
Real poll timestamps, ~100ms apart, with no override |
05-kafka-listener-await.txt |
await() for an @KafkaListener to consume a record just sent |
06-scheduled-job-await.txt |
await() for 3 more executions of an already-running @Scheduled job |
07-uncaught-exception-handler-swap.txt |
Proof that await() swaps and restores the JVM's default uncaught-exception handler |
08-exception-propagates-immediately.txt |
Real failure: an exception from the polled condition, with no ignoreExceptionsInstanceOf(...), failing on the first poll (test since removed) |
09-ignore-exceptions-waits-it-out.txt |
The fixed version: the same resource, with ignoreExceptionsInstanceOf(...) |
10-missing-kafka-starter-failure.txt |
Real failure: the Kafka test with spring-kafka as a direct dependency instead of spring-boot-starter-kafka (pom.xml since fixed) |
11-pom-diagnosis.txt |
The real curl/grep transcript that found the Boot 4.1 Kafka module split against Maven Central's own POMs |
What's sourced from documentation, not run here
Awaitility's own @since Javadoc tags, Spring Boot's own migration notes for the Kafka module
split, and the general shape of @KafkaListener/@EmbeddedKafka wiring are cited from the real
jars and POMs on Maven Central (see the post for exact artifact coordinates), not re-derived from
scratch in this module.
Licence
MIT — see LICENSE.