Assertions
Awaiting async writes
awaitAtMost: a bounded collection assertion for writes that happen on another thread. MongoTestAwait remains the standalone helper for conditions and query waits.
Seed, act, await, assert
Listeners, @Async services and outbox pollers all write to Mongo on someone else's thread. Polling for the effect is the only honest way to assert on it. A fixed Thread.sleep is either too short on CI or too slow everywhere else.
// seed
MongoTestData.insert("orders", order);
// act: the listener writes on its own thread
publisher.publish(orderPlaced);
// await: block until the effect lands
MongoTestAwait.untilExists("outbox", "{event: 'ORDER_PLACED'}");
// assert: now the state is stable enough to interrogate
assertThatCollection("outbox").containsDocumentSatisfying(d -> d
.hasField("orderId", "ord-1"));awaitAtMost
Make the collection claim itself wait. Everything chained after awaitAtMost (3.5.0+) retries against a freshly read collection until it passes or the deadline expires.
publisher.publish(orderPlaced);
assertThatCollection("outbox")
.awaitAtMost(Duration.ofSeconds(15))
.pollingEvery(Duration.ofMillis(100))
.containsExactlyOneDocumentSatisfying(d -> d
.hasField("orderId", "ord-1"));pollingEvery changes the default 100 ms cadence. Only assertion failures retry; store errors and matcher exceptions propagate immediately. A timeout includes the final full failure plus attempt count and cadence. There is no global default timeout, and extractingField() and documents() refuse while polling is armed rather than silently reading once.
MongoTestAwait
Use the standalone helper when the condition is not a fluent collection assertion, or when the useful wait is a count or a real query filter. Its helpers remain available and truthful alongside awaitAtMost.
untilCount
Waits until the collection holds exactly the given number of documents. Note the exactness: if a late writer pushes the count past the target between polls, the wait keeps going and eventually times out. That is itself a finding.
MongoTestAwait.untilCount("orders", 3);untilExists and untilAbsent
untilExists waits until at least one document matches a filter; untilAbsent waits until none does. Filters are real query documents parsed with Document.parse and evaluated by the same engine the application's own queries go through. A filter that matches here matches in production code too, and an operator the emulation refuses fails fast instead of silently never matching.
// The router marked the order: a real query document, same engine as production
MongoTestAwait.untilExists("orders", "{status: 'ROUTED'}");
// The poller drained the outbox
MongoTestAwait.untilAbsent("outbox", "{processed: false}");until: arbitrary conditions
For a condition that is not a collection count, until polls any BooleanSupplier. The description string completes the sentence “waiting for <…>” in the timeout message, so phrase it as a noun.
// The description completes "waiting for <...>", so phrase it as a noun
MongoTestAwait.until(() -> service.isReconciled(), "reconciliation");
MongoTestAwait.until(() -> queue.isDrained(), "the outbox drain to finish");Timeouts and polling
Defaults: 10 seconds before timing out, polling every 50 ms. The condition is always tried at least once, so a wait that is already satisfied returns immediately even with a tiny timeout.
// Every helper has a Duration overload for the slow case
MongoTestAwait.untilCount("outbox", 1, Duration.ofSeconds(30));
MongoTestAwait.untilExists("orders", "{status: 'SHIPPED'}", Duration.ofSeconds(30));
MongoTestAwait.untilAbsent("staging", "{state: 'PENDING'}", Duration.ofMinutes(1));
MongoTestAwait.until(() -> batch.isDone(), "the nightly batch", Duration.ofMinutes(2));A negative timeout is an IllegalArgumentException; a null one means the default. A thread interrupt during the wait re-asserts the interrupt flag and fails the test rather than swallowing it.
What a timeout shows
A bare “timed out” tells you nothing you did not already know, so a timeout renders what the collection actually held at that moment. The same fixed-width table the assertion failures print, capped at 20 documents for an await. The format:
Timed out after PT10S waiting for 3 documents in <test.orders> matching <ALL>. Found 1.
Collection state at timeout:
idx | _id | document
----+--------------------------+----------------------------------------
0 | 665f1e2a9d1e8a4b3c2d1f00 | {"status": "NEW", "amount": 4999}<ALL> marks the no-filter case; a filtered wait restates its filter. The table is usually the whole diagnosis: you see immediately whether the document is missing, or present with the wrong field. That is the difference between “the listener never fired” and “the listener fired and wrote the wrong thing”.