Assertions

Choreography assertions

assertThatChoreography(): for saga, outbox, and event-choreography tests, where the question is not “is this collection right?” but “did the flow touch every collection it should have?”

The shape of a flow test

A flow test cares that one logical entity, such as an order, a payment, or a correlation id, left traces in several collections, possibly across several databases. Asserting that as four separate blocks buries the intent; the choreography assertion states it in one chain.

Java
assertThatChoreography()
    .forId("ord-123")                        // matches _id, or any field carrying the value
    .collection("orders").contains()
    .collection("outbox").contains()
    .collection("billing.invoices").contains()   // another database; same chain
    .verify();

forId

Declares the id the flow is about: a document's _id, or the value of a key field such as orderId. It must be called before naming a collection; doing otherwise is an IllegalStateException, because a choreography step means “this id reached this collection”. It may be called again mid-chain to switch ids. Each step remembers the id in force when it was declared, and a mixed-id chain is reported as the flow across N ids.

contains and doesNotContain

collection(name).contains() records the expectation that the collection holds at least one document carrying the id. doesNotContain() records the opposite. This is the compensating half of a saga test, and the assertion that a rollback really rolled back.

Java
// The compensating half of a saga test: the rollback really rolled back
assertThatChoreography()
    .forId("ord-123")
    .collection("orders").doesNotContain()
    .collection("billing.invoices").doesNotContain()
    .collection("dead_letters").contains()
    .verify();

verify: all steps, one report

Nothing runs until verify(). The chain only records steps; verify() reads every named collection once and reports all of them together. That is the whole point: a flow test that dies on the first missing collection tells you one thing, and one that shows the whole flow tells you where it stopped. The report is the same fixed-width table the other assertions use, with ▶ on each failed step. For example:

text
Expected the flow for id <ord-123> to be complete, but 1 of its 3 steps failed.
    | idx | collection            | documents | id found | note
  --+-----+-----------------------+-----------+----------+--------------------------------------
    | 0   | test.orders           | 1         | yes      | matched _id of document 0
    | 1   | test.outbox           | 2         | yes      | matched field <orderId> of document 0
  ▶ | 2   | billing.invoices      | 0         | no       | collection is empty

The note column speaks a small fixed vocabulary: matched _id of document 0, matched field <orderId> of document 0, collection is empty, no document carries the id, absent, as expected, and for a doesNotContain() step that found the id, matched field <orderId> of document 0, but the id must be absent here. A collection that cannot be read at all becomes a failed step noting the exception, rather than an error that hides the rest of the report.

Calling verify() with no recorded steps is an IllegalStateException. A chain that asserts nothing should say so rather than pass silently.

How an id is matched

_id first, then any top-level field whose value equals the id. The same business key is an _id in the collection that owns it and a plain field (orderId, aggregateId, correlationId) everywhere downstream. Making the test name that field per collection would be noise. The note column always says which field actually matched, so the match is never a mystery. Numbers compare by value across BSON types, exactly as they do in the document assertions.