Postboy Help

Testing: Assertions and History

world.then verifies what the system under test communicated. All checks are synchronous: they inspect the history recorded up to that point and throw a plain Error on failure. world.history exposes the same records directly for custom inspection, and PostboyWorldVerifier offers the same checks as booleans.

then.fired(type)

Returns a PostboyFiredThen<T> builder over the recorded messages of that type.

Exact counts:

world.then.fired(OrderPlacedEvent).once(); world.then.fired(OrderPlacedEvent).times(3); world.then.fired(OrderPlacedEvent).atLeast(2);

Payload predicates:

// .with passes when ANY recorded message matches world.then.fired(OrderPlacedEvent).with((e) => e.orderId === 42); // .first / .last check only the earliest / most recent message world.then.fired(OrderPlacedEvent).first((e) => e.phase === 'START'); world.then.fired(OrderPlacedEvent).last((e) => e.phase === 'FINISH');

Reading a message:

// The LAST recorded instance; throws if none was fired const last = world.then.fired(OrderPlacedEvent).value; expect(last.tax).toBe(0.2);

Chaining assertions - .and() returns the then service for the next check:

world.then .fired(OrderPlacedEvent) .once() .with((e) => e.status === 'OK') .and() .subscribed(OrderPlacedEvent) .atLeast(1);

then.notFired(type)

Throws when any message of the type was recorded. Use it for negative cases after the act.

service.place(42); world.then.notFired(OrderFailedEvent);

then.subscribed(type)

Every sub or once call on the mock bus increments a counter per type. subscribed returns a builder over that counter: once(), times(n), atLeast(n), and .and().

world.then.subscribed(OrderPlacedEvent).times(2);

Subscriptions are counters - .with, .first, and .last do not exist here.

MessageHistory

world.history is the store behind then. Reach for it when the declarative API is not enough: ordering, index access, or custom filtering.

const orders = world.history.messages(OrderPlacedEvent); orders.all; // T[] snapshot, in fire order orders.first; // earliest instance or null orders.last; // most recent instance or null orders.length; // recorded count orders.has((e) => e.amount > 1000); // any match orders.hasItem(firedInstance); // reference check

Callback messages record their results too:

// Collection of { message, result } pairs, oldest first const results = world.history.callbackResults(FetchUserQuery); expect(results.last?.result).toEqual(fakeUser); // Hot stream of finish() results - subscribe before the act const seen: string[] = []; world.history.callbackResult$(FetchUserQuery).subscribe(({ result }) => seen.push(result));

Subscription counts and reset:

world.history.subs(OrderPlacedEvent); // number of sub + once calls world.history.reset(); // clears records; world.dispose() also does this

PostboyWorldVerifier

Non-throwing boolean counterparts. The world has no verifier getter - construct it over the world's history:

import { PostboyWorldVerifier } from '@artstesh/postboy-testing'; const verifier = new PostboyWorldVerifier(world.history); if (verifier.fired(OrderPlacedEvent)) { // fired at least once } verifier.fired(OrderPlacedEvent, 2); // fired exactly twice verifier.subscribed(OrderPlacedEvent, 1); // subscribed exactly once

Prefer then for straight assertions - it fails with a descriptive error. Use the verifier inside conditional test logic where a boolean fits better.

Pitfalls

  • then.fired(X) returns a builder, not a boolean. A bare world.then.fired(X) asserts nothing - finish the chain with .once(), .times(n), or a similar check.

  • .value is the last message, not the first. Use history.messages(X).first for the earliest.

  • .with scans the whole collection; combine it with .once() when exactly one matching message must exist.

  • Assertions are synchronous. For messages that arrive later, wait first - see Async waiters.

Next steps

07 September 2026