Add awaitility module: testing async @Async/@Scheduled/@KafkaListener code without Thread.sleep

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.
This commit is contained in:
Claude
2026-10-08 15:18:58 +00:00
parent 09631dcaab
commit a7b76243fa
31 changed files with 900 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# awaitility
Companion code for the Awaitility article on [ankurm.com](https://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](../LICENSE).