Assertions
Assertions
A fluent assertion layer over the store, built on AssertJ with Hamcrest interop. The design goal is that a failure tells you what happened without a debugger.
One entry point
MongoAssertions is the single static-import surface. Assertions live in the generic bonsai-junit module, not the Spring one. They only need a client, so they work in either harness. They read the store through the active @EnableMongoEmulator context, so there is no client to pass and no fixture to hold on to.
import static io.typecheck.bonsai.assertions.MongoAssertions.assertThatCollection;
import static io.typecheck.bonsai.assertions.MongoAssertions.assertThatDocument;
import static io.typecheck.bonsai.assertions.MongoAssertions.assertThatChoreography;Everything chains
Every collection assertion method returns the same MongoCollectionAssert, and every document assertion method returns the same MongoDocumentAssert. Keep related checks in one fluent expression; the consumer passed to containsDocumentSatisfying, containsExactlyOneDocumentSatisfying, allDocumentsSatisfy, or noDocumentSatisfies is a document chain too.
assertThatCollection("orders")
.exists()
.hasDocumentCount(1)
.containsExactlyOneDocumentSatisfying(d -> d
.hasId("ord-1")
.hasField("status", "CONFIRMED")
.hasJsonPath("$.total", 4999)
.doesNotHaveField("error"))
.noDocumentSatisfies(d -> d.hasField("status", "FAILED"));Prefer one chain over several assertThatCollection(...) statements. The flat form re-reads the collection for every statement and a failure only reports that one check, instead of keeping the related claim together.
The five families
The DSL is small enough to hold in your head, but each family has its own semantics and its own failure format, so each gets its own page:
- Collection assertions: counts, existence, indexes, field-value checks, and per-document conditions over a collection that is re-read on every call.
- Document assertions: fields, ids, JsonPaths, nested document arrays, and whole-document JSON comparison on a single document.
- Change tracking: assertions over a seeded document's baseline and its current state.
- Field matchers: Hamcrest matchers for generated values: ObjectIds, UUIDs, timestamps, BSON dates, decimal128.
- Ordered snapshots: “exactly these documents, in this order”, with explicit sorting and ignorable fields.
- Choreography: one id traced across several collections, reported as a single table.
Since 3.4.0 the vocabulary has a Hamcrest form almost everywhere it took a value or a lambda: hasDocumentCount(greaterThan(3L)), the four …Satisfying quantifiers with a Matcher<Document>, hasFieldValues, hasIdMatching, hasJsonPathMatching, matchesDocument, and MongoDocumentMatchers The whole document vocabulary is available as standalone matchers for composing with allOf/not or for slotting Bonsai checks into a Hamcrest-native suite. Two rules keep it unsurprising: a …Matching name takes a matcher where its sibling takes a literal, and …Matching(String) on a collection stays the server-side query.
The awaiting page covers awaitAtMost and MongoTestAwait, the bounded polling patterns that make asserting after an asynchronous write honest. The bare method-by-method signatures are in the Assertions API reference.
A quick tour
Collection assertions read lazily, so they always see the current state of the store. Collection names follow the same rule as everywhere else: unqualified names resolve against the default database, and a dot targets a specific one.
assertThatCollection("orders") // or "billing.invoices"
.exists()
.hasDocumentCount(2)
.hasDocumentCount(1, d -> d.hasField("status", "CONFIRMED"))
.hasDocumentCountAtLeast(1)
.hasDocumentCountAtLeast(1, d -> d.hasField("customerId", "cust-42"))
.hasIndexOn("customerId")
.hasUniqueIndexOn("orderNumber")
.containsDocumentMatching("{status: 'CONFIRMED', amount: {$gt: 100}}")
.containsDocumentSatisfying(d -> d
.hasField("status", "CONFIRMED")
.hasJsonPath("$.lines[0].sku", "WIDGET-1"))
.allDocumentsSatisfy(d -> d.hasField("tenantId", "t-1"))
.noDocumentSatisfies(d -> d.hasField("status", "FAILED"));
// Drop into AssertJ for one field across every document
assertThatCollection("orders")
.extractingField("status")
.containsExactlyInAnyOrder("CONFIRMED", "SHIPPED");
// Or take an ordered snapshot
assertThatCollection("audit_log").documents();Document assertions work on a document you already hold, usually one handed to you by containsDocumentSatisfying:
assertThatDocument(order)
.hasId("ord-1")
.hasField("status") // present, any value
.hasField("status", "CONFIRMED") // present, equal
.hasField("customer.tier", "GOLD") // dotted paths walk nested docs
.doesNotHaveField("cancelledAt")
.hasFieldMatching("orderNumber", matchesRegex("ORD-\\d{8}"))
.hasJsonPath("$.lines[0].qty")
.hasJsonPath("$.lines[0].qty", 2)
.matchesJson("""
{"_id": "ord-1", "status": "CONFIRMED"}
""", "$.placedAt", "$.lines"); // ignoring these pathsOrdered snapshots compare a whole collection structurally, in an order the test names explicitly:
assertThatCollection("audit_log")
.documents()
.sortedBy("timestamp") // collections are unordered; sort explicitly
.ignoringFields("_id", "createdAt") // field names or JsonPaths, your choice
.containsExactlyJson(
"""
{"event": "ORDER_PLACED", "orderId": "ord-1"}
""",
"""
{"event": "PAYMENT_TAKEN", "orderId": "ord-1"}
""");Choreography traces one id across every collection a flow should have touched:
assertThatChoreography()
.forId("cust-42")
.collection("orders").contains()
.collection("outbox").contains()
.collection("billing.invoices").contains()
.collection("dead_letters").doesNotContain()
.verify();Field matchers assert generated values by shape rather than by value:
import static io.typecheck.bonsai.assertions.MongoFieldMatchers.*;
assertThatDocument(order)
.hasFieldMatching("_id", validObjectIdHex())
.hasFieldMatching("traceId", validUuid())
.hasFieldMatching("placedAt", validIsoInstant())
.hasFieldMatching("createdAt", bsonDate())
.hasFieldMatching("amount", decimal128(greaterThan(new BigDecimal("10"))))
.hasFieldMatching("notes", nonBlankString())
.hasFieldMatching("orderNumber", matchesRegex("ORD-\\d{8}"))
.hasFieldMatching("payload", validJson())
.hasFieldMatching("payload", jsonPath("$.status", equalTo("CONFIRMED")));And MongoTestAwait holds the assertion back until an asynchronous write has actually landed:
MongoTestAwait.untilCount("outbox", 1);
MongoTestAwait.untilExists("orders", "{status: 'SHIPPED'}");
MongoTestAwait.untilAbsent("staging", "{state: 'PENDING'}");
MongoTestAwait.until(() -> queue.isDrained(), "the outbox drain to finish");
// Every helper takes an explicit timeout too
MongoTestAwait.untilCount("outbox", 1, Duration.ofSeconds(30));Failure messages
A failing assertion dumps the collection as a fixed-width table and abbreviates long values so the message stays readable. Both dumps below are genuine output, captured from a real failing test against the shim. First, a wrong count. Note the long note value cut at the table's value width with …:
java.lang.AssertionError:
Expected collection <test.orders> to hold 3 documents, but it holds 2.
Collection state:
idx | _id | document
----+-------+-------------------------------------------------------------------------------------------------
0 | ord-1 | {"status": "CONFIRMED", "amount": 4999, "customerId": "cust-42"}
1 | ord-2 | {"status": "NEW", "amount": 129900, "customerId": "cust-42", "note": "Gift order — please wrap …Second, an ordered-snapshot mismatch. The diff table marks the first mismatching row with ▶ so the eye lands on the offending row before it starts reading columns:
java.lang.AssertionError:
Expected collection <test.audit_log> to contain exactly 2 documents (sorted by <seq>, ignoring $._id, $.seq), but they differ at index 1.
| idx | expected | actual
--+-----+--------------------------------------+------------------------------------
| 0 | {event=ORDER_PLACED, orderId=ord-1} | {event=ORDER_PLACED, orderId=ord-1}
▶ | 1 | {event=PAYMENT_TAKEN, orderId=ord-1} | {event=REFUNDED, orderId=ord-1}The dump is on by default and capped at 50 documents, so it informs without flooding the log. silentOnFailure() turns it off per assertion when the collection is large and irrelevant to the failure; an await that times out prints the same table, capped at 20. The details are on each family's page.