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
@@ -0,0 +1,14 @@
[INFO] Running com.ankurm.awaitility.KafkaListenerAwaitTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.012 s -- in com.ankurm.awaitility.KafkaListenerAwaitTest
[INFO] Running com.ankurm.awaitility.ScheduledJobAwaitTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.841 s -- in com.ankurm.awaitility.ScheduledJobAwaitTest
[INFO] Running com.ankurm.awaitility.UncaughtExceptionHandlerSwapTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.115 s -- in com.ankurm.awaitility.UncaughtExceptionHandlerSwapTest
[INFO] Running com.ankurm.awaitility.AwaitAsyncConfirmationTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.654 s -- in com.ankurm.awaitility.AwaitAsyncConfirmationTest
[INFO] Running com.ankurm.awaitility.IgnoreExceptionsTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.636 s -- in com.ankurm.awaitility.IgnoreExceptionsTest
[INFO] Running com.ankurm.awaitility.DefaultTimingTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.70 s -- in com.ankurm.awaitility.DefaultTimingTest
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
@@ -0,0 +1,9 @@
== Thread.sleep(100) against a confirmation that actually takes 220ms (real failure, test since removed) ==
$ mvn -B -Dtest=SleepGuessesWrongTest test
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 3.568 s <<< FAILURE! -- in com.ankurm.awaitility.SleepGuessesWrongTest
com.ankurm.awaitility.SleepGuessesWrongTest.confirmationArrivesWithin100ms -- Time elapsed: 1.084 s <<< FAILURE!
java.lang.AssertionError:
Expecting actual not to be null
at com.ankurm.awaitility.SleepGuessesWrongTest.confirmationArrivesWithin100ms(SleepGuessesWrongTest.java:27)
@@ -0,0 +1,6 @@
== await().untilAsserted() against a 220ms-delayed void @Async method ==
mailbox.confirmationFor("ORD-CAPTURE") immediately before sendConfirmation(): null
await().atMost(2s).untilAsserted(...) returned after approximately: 307ms
mailbox.confirmationFor("ORD-CAPTURE") once await() returns: order ORD-CAPTURE confirmed
@@ -0,0 +1,3 @@
== await().until(() -> false) with no atMost(...) override ==
elapsed before ConditionTimeoutException: 10057ms (expected: ~10,000ms)
@@ -0,0 +1,6 @@
== await().atMost(550ms).until(...) with no pollInterval(...) override ==
poll timestamps (ms since start): [107, 207, 308, 409, 509]
gaps between consecutive polls (ms): [100, 101, 101, 100]
average gap: 100.5ms (expected: ~100ms)
@@ -0,0 +1,6 @@
== await() for an @KafkaListener to consume a record a KafkaTemplate just sent, against an @EmbeddedKafka broker ==
listener.received() immediately after send() returned: (not checked -- that is the point)
listener.received() once await() returned: [order-77-created]
approximate elapsed time: 378ms
@@ -0,0 +1,7 @@
== await() for 3 more @Scheduled(fixedRate = 150) executions, regardless of how long the job had already been running ==
runCount() when the test started: 1
target (baseline + 3): 4
runCount() once await() returned: 4
approximate elapsed time: 510ms
@@ -0,0 +1,6 @@
== Thread.getDefaultUncaughtExceptionHandler() before, during and after one await() call ==
handler during await() is the one this test installed: false (expected: false)
handler during await() is Awaitility's own (class org.awaitility.core.CallableCondition$1)
handler after await() is the one this test installed: true (expected: true)
@@ -0,0 +1,11 @@
== await().untilAsserted(...) with no ignoreExceptionsInstanceOf(...), against a resource that throws for its first 300ms (real failure, test since removed) ==
$ mvn -B -Dtest=ExceptionPropagatesImmediatelyTest test
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.940 s <<< FAILURE! -- in com.ankurm.awaitility.ExceptionPropagatesImmediatelyTest
com.ankurm.awaitility.ExceptionPropagatesImmediatelyTest.failsOnTheFirstPollInsteadOfWaiting -- Time elapsed: 0.946 s <<< ERROR!
java.lang.IllegalStateException: resource is still warming up
at com.ankurm.awaitility.FlakyStartupResource.value(FlakyStartupResource.java:26)
at com.ankurm.awaitility.ExceptionPropagatesImmediatelyTest.lambda$failsOnTheFirstPollInsteadOfWaiting$0(ExceptionPropagatesImmediatelyTest.java:28)
Note "Errors: 1", not a ConditionTimeoutException after the full atMost(1s) window: the exception
from the very first poll propagated straight out instead of being retried.
@@ -0,0 +1,3 @@
== await().ignoreExceptionsInstanceOf(IllegalStateException.class) against a resource that throws for its first 300ms ==
approximate elapsed time before resource.value() finally returned "ready": 302ms (expected: a little over 300ms)
@@ -0,0 +1,23 @@
== Real failure with org.springframework.kafka:spring-kafka as a direct dependency instead of org.springframework.boot:spring-boot-starter-kafka (pom.xml since fixed) ==
$ mvn -B test
[ERROR] Errors:
[ERROR] KafkaListenerAwaitTest.messageSentIsEventuallyConsumed:49 ? ConditionTimeout Assertion condition defined as a Lambda expression in com.ankurm.awaitility.KafkaListenerAwaitTest
Expecting ConcurrentLinkedQueue:
[]
to contain:
["order-42-created"]
but could not find the following element(s):
["order-42-created"]
within 5 seconds.
[INFO]
[ERROR] Tests run: 11, Failures: 0, Errors: 2, Skipped: 0
No exception at context startup, and the application context came up without a single warning.
Autowiring KafkaListenerEndpointRegistry directly in a scratch test and calling
getListenerContainers() on it confirmed the real cause: the bean did not exist. Neither did
KafkaTemplate, ConsumerFactory, or kafkaListenerContainerFactory. Grepping the full log for the
Spring Kafka logger prefix also confirmed it:
$ grep -c "o\.s\.k\." full-test-run.log
0
@@ -0,0 +1,38 @@
== Confirming the Boot 4.1 Kafka module split directly against Maven Central's real POMs ==
$ curl -sS https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/4.1.1/spring-boot-dependencies-4.1.1.pom -o spring-boot-dependencies-4.1.1.pom
$ grep -B3 '<artifactId>spring-boot-kafka</artifactId>' spring-boot-dependencies-4.1.1.pom
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-kafka</artifactId>
$ curl -sS https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-kafka/4.1.1/spring-boot-starter-kafka-4.1.1.pom | grep -A2 '<artifactId>'
<artifactId>spring-boot-starter-kafka</artifactId>
<version>4.1.1</version>
<name>spring-boot-starter-kafka</name>
--
<artifactId>spring-boot-starter</artifactId>
<version>4.1.1</version>
<scope>compile</scope>
--
<artifactId>spring-boot-kafka</artifactId>
<version>4.1.1</version>
<scope>compile</scope>
$ curl -sS https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-kafka/4.1.1/spring-boot-kafka-4.1.1.pom | grep -A2 '<artifactId>'
<artifactId>spring-boot-kafka</artifactId>
<version>4.1.1</version>
<name>spring-boot-kafka</name>
--
<artifactId>spring-boot</artifactId>
<version>4.1.1</version>
<scope>compile</scope>
--
<artifactId>spring-boot-transaction</artifactId>
<version>4.1.1</version>
<scope>compile</scope>
--
<artifactId>spring-kafka</artifactId>
<version>4.1.1</version>
<scope>compile</scope>