All releases

v3.5.0

**Nested document-array assertions.** `inArray(field)` on the document assert opens a scoped chain for state stored as nested document ar...

Full notes

Added

  • Nested document-array assertions. inArray(field) on the document assert opens a scoped chain for state stored as nested document arrays: matching(field, value) selects exactly one element (zero and many fail listing candidates by index, multi-key varargs disambiguate), at(index), single() and hasSize(n) complete the scope, the element scope is the full document vocabulary so nesting recurses, and every failure carries the full path from the collection to the failing assertion. Lambda overloads branch to several elements of one array. This replaces JsonPath filter expressions for the everyday case; a failed hasJsonPath on a filter or wildcard path now teaches the returns-an-array rule with working alternatives.
  • Polling assertions. awaitAtMost(Duration) retries everything chained after it against a freshly read collection until it passes or the deadline expires (pollingEvery tunes the cadence, clamped to the timeout; sleeps never overshoot the deadline). Only the collection assertion's own failures retry; the timeout failure carries the last attempt's message plus the attempt count. The handover methods extractingField() and documents() refuse while polling is armed rather than silently running once, and the javadoc states that a poll cannot prove a negative.
  • Change tracking. Both harnesses snapshot each seeded collection once per test method, after seeding and before the test body. documentWithId(id).changedField(path).from(v).to(w) proves the transition, not just the end state; changedField(path) alone already asserts the field existed at baseline and actually changed, unchangedField, createdField and removedField complete the vocabulary, and paths take dots and [index] hops. Selection is a snapshot by _id: a document created during the test refuses for lacking a baseline, while a same-id delete-and-reinsert compares as an update. Capture is capped (bonsai.change-tracking.max-documents, default 50000, named refusal when tripped) and can be disabled with bonsai.change-tracking=off.
  • `present()` field matcher. Completes the presence ladder: hasField(name) is existence (explicit null included), hasFieldMatching(name, present()) is existence with a non-null value of any BSON type, typed matchers assert shape. The failure distinguishes an absent field from an explicit null. A bare present() in the list-valued assertions is refused with everyItem(present()) guidance.
  • `allFieldValues(name, matcher)`. One Hamcrest matcher over every document's value for a field, in store order, with offending documents attributed by index in the failure.
  • Compound and TTL index assertions. hasCompoundIndexOn(fields...) asserts an index with exactly those ascending keys in that order (no prefix credit; the failure names prefix, reversed-order and single-field near-misses), the Document overload pins directions with BSON-numeric key comparison, and hasTtlIndexOn(field, duration) matches expireAfterSeconds exactly.
  • Per-invocation seeding context. SeedContext.testMethod() carries the test method name, resolved parameterized-test arguments and invocation ordinal for method-scoped seeds, empty at class and suite scope, so one SeedSource serves every @ParameterizedTest invocation.
  • Registered placeholder resolvers. Any PlaceholderResolver bean in the Spring test context (or classes on @EnableMongoEmulator(placeholderResolvers = ...)) answers fixture placeholders. The engine imposes the chain: explicit bindings, then built-ins, then registered resolvers, then the Environment fallback, so ambient configuration never shadows a declared resolver and shadowing a built-in name is a startup error. Each name resolves once per seed application and reads back via MongoTestSupport.placeholderValue(name), cleared per method; suite-level seeds resolve through the same chain, and recordings attribute resolver-computed values by bean name in placeholderSources.
  • Change-stream server-version profile (experimental). bonsai.experimental.change-streams.server-version=7.0|8.0 (default 8.0) emits the chosen real server's event shape. The registry holds the one proven difference: on 7.0 an $addToSet append reports the whole resulting array in updatedFields, top-level and nested alike, where 8.0+ reports the appended index. Other values refuse by name (changeStream.serverVersion); each profile is verified strictly against a real server of the matching version in the differential lane.

Changed

  • Change-stream event parity campaign. The differential lane now compares the change events the two backends publish, not just state: 1,029 checks green against real mongod 7.0, 8.0 and 8.3, with 28 divergences found and fixed (fullDocument field order, updateDescription granularity and emission order, removedFields ordering, deleteMany event order, commit-order delivery under concurrent writers). Two proven server behaviours are pinned honestly instead of imitated: a nondeterministic upsert seed order and the 7.x whole-array $addToSet delta.
  • Collection assertions' nested document failures now lead with the collection name in the path.
  • The documentation site, skill guides and shipped examples were rewritten to remove em-dash prose, and the change-streams pages now lead with a highly-experimental warning recommending a real server where possible.

Fixed

  • A @Nested class declaring placeholderResolvers is refused loudly instead of silently using the enclosing class's resolvers; the change-tracking baseline and class-seed placeholder readback survive bonsai.cleanup=clear and =none; the pre-8 comparison tolerance arms on the CI service-backed differential lane; a broken javadoc reference failed the release javadoc jar.