Postboy Help

Core Concepts

Understanding the building blocks of @artstesh/postboy-testing will help you write tests that are both powerful and easy to maintain.
This section introduces the central orchestrator, the flow of a test, and the key abstractions you’ll use every day.

1. The PostboyWorld hub

PostboyWorld is the single entry point for all testing operations.
When you create it, a fully wired testing environment is born:

const world = new PostboyWorld();

Inside, it assembles six cooperating pieces:

PostboyWorldPostboyServiceMockMessageHistoryPostboyWaiterServicePostboyGivenServicePostboyThenServicePostboyMessageStreamServiceRegistrywrites every actionuses to mockfires / configuresreads recorded datasubscribes for future messagesoptional lookback(includeHistory)registers messages 

You never need to construct these services manually. The world gives you ready‑to‑use properties that already talk to each other.

2. The rhythm of a test

Tests follow a clear Arrange – Act – Assert pattern, with each phase supported by a specific part of the toolkit.

TesterTesterworld.givenworld.givenService under testService under testworld.postboyworld.postboyworld.thenworld.thenworld.waiterworld.waiterArrangesetup mock (event, callback, executor)fire pre‑event or prepare streaminject into SUTActtrigger behaviourfire / exec / fireCallbackAssertcheck history (sync)reads recorded actionswait for future message (async)subscribe and await
  • Arrange – use world.given to prepare the mock environment and inject world.postboy into your code.

  • Act – call the method you’re testing; the mock bus records every interaction.

  • Assert – verify the recorded history with world.then or wait for asynchronous outcomes with world.waiter.

3. The mock bus (world.postboy)

world.postboy is a drop‑in replacement for the real PostboyService.
It behaves exactly like the production bus, but it also writes a detailed log of everything that happens.

When your code calls fire(), exec(), fireCallback(), sub(), or once(), the mock bus:

  1. Adds an entry to the internal MessageHistory.

  2. Then forwards the call to the real underlying mechanism.

This means you can check what was communicated after the test action — without any spies.

4. The recorded history (world.history)

Every message sent and every subscription created is recorded in a MessageHistory object.
It gives you direct access when you need it:

const allOrders = world.history.messages(OrderPlacedEvent).all; const numberOfSubscriptions = world.history.subs(OrderPlacedEvent);

The history is organised by message type.
Each type gets its own HistoryCollection<T> with methods like first, last, all, has(predicate), and length.

world.then reads from exactly this history, so you rarely need to touch it directly — but it’s always there for edge cases.

5. Arrange helpers (world.given)

world.given is a BDD‑style API that lets you prepare the mock behaviour before the act.

  • given.event(msg) – fires a replay message immediately (useful for setting up state).

  • given.callback(Type, result) – when the tested code sends a callback message, the mock automatically answers with result.

  • given.executor(Type, result) – when an executor is invoked, the mock returns result.

All these methods return this, so you can chain them:

world.given .event(new UserLoggedInEvent(123)) .executor(GetUserExecutor, fakeUser);

Under the hood, given delegates to the low‑level world.mocks (the PostboyMessageStreamService), but you should prefer the clean given interface for typical test scenarios.

6. Assert helpers (world.then)

world.then provides declarative assertions over the recorded history.
It reads from the same history that world.history exposes, but throws clear errors when expectations are not met.

  • then.fired(Type) → verify sent messages
    .once(), .times(n), .atLeast(n), .with(predicate), .first(predicate), .last(predicate), .value

  • then.notFired(Type) → assert a message was never sent

  • then.subscribed(Type) → verify subscription counts
    .once(), .times(n), .atLeast(n)

You can chain multiple checks together with .and():

world.then .fired(OrderPlacedEvent).once() .and() .subscribed(OrderPlacedEvent).atLeast(1);

All assertions are synchronous — they inspect the history that has already been recorded up to that point.

7. Async waiter (world.waiter)

When a message is emitted inside a subscription, microtask, or timeout, you can’t check the history right away.
That’s where world.waiter steps in — it subscribes to the future message stream and resolves a promise when the expected message arrives (or when a timeout expires).

  • waiter.waitFor(Type) – wait for the next matching message.

  • waiter.waitForMany(Type, count) – wait for several messages.

  • waiter.waitForCallbackResult(Type) – wait for a callback response.

  • waiter.waitForNone(Type) – assert silence for a given duration.

  • waiter.waitForAny(types[]) – wait for any of the given types.

By default, the waiter ignores messages that were already sent before the call.
Pass includeHistory: true if the event could have happened earlier:

service.placeOrder(42); const event = await world.waiter.waitFor(OrderPlacedEvent, {includeHistory: true});

8. Strict vs. non‑strict mode

The world can run in two modes, controlled by the PostboyTestingSettings option.

Mode

Setting

Behavior

Non‑strict (default)

{ strict: false }

No pre‑registration required. Messages work out of the box.

Strict

{ strict: true }

Every message type must be explicitly registered via world.registry. Missing registrations cause errors.

Non‑strict is great for quick tests and prototyping.
Strict mode gives you extra safety — it mirrors the registration requirements of a well‑structured production app and catches missing configurations early.

To register types in strict mode:

// use the given mechanism world.given .event(new UserLoggedInEvent(123)) .executor(GetUserExecutor, fakeUser); // or the registry for manual registration world.registry.recordSubject(OrderPlacedEvent); world.registry.recordReplay(UserSessionEvent, 1); world.registry.recordExecutor(GetOrderExecutor, (exec) => 22);

9. Lifecycle management: dispose() is mandatory

PostboyWorld creates internal subscriptions and a dedicated namespace inside the mock bus.
If you don’t call dispose() after each test, those subscriptions and namespaces will leak into the next test, causing cross‑contamination and unpredictable behaviour.

Always pair your world with afterEach:

let world: PostboyWorld; beforeEach(() => { world = new PostboyWorld(); }); afterEach(() => { world.dispose(); // ← REQUIRED });

dispose() clears the message history, unsubscribes everything, and removes the namespace.
It gives you a clean slate for every test.

Next steps

Now that you understand the foundations, dive deeper into each area:

You’re holding a complete testing toolkit. The rest of the documentation will turn you into an expert.

07 мая 2026