Postboy Help

Testing: PostboyWorld

PostboyWorld is the single fixture of @artstesh/postboy-testing. It constructs a recording mock bus, a message history, stubbing, assertion, and waiter services, plus a private namespace registrator, and wires them together. Tests never build these pieces manually; they read them off the world through getters.

import { PostboyWorld } from '@artstesh/postboy-testing'; const world = new PostboyWorld(); // same as new PostboyWorld({ strict: false })

Anatomy

Getter

Type

Purpose

postboy

PostboyServiceMock

The mock bus. Extends the real PostboyService, so every bus method works; fire, exec, sub, once, and fireCallback additionally record. Inject it into the SUT.

history

MessageHistory

Recorded messages, callback results, and subscription counters.

given

PostboyGivenService

Arrange-phase stubs: executor, callback, event. All chainable.

then

PostboyThenService

Assert-phase checks: fired, notFired, subscribed.

waiter

PostboyWaiterService

Async waits: waitFor, waitForMany, waitForAny, waitForCallbackResult, waitForNone, delay.

mocks

PostboyMessageStreamService

Low-level stream mocks; the layer given delegates to.

registry

PostboyAbstractRegistrator

Namespaced registrator for manual registration.

Strict vs non-strict

interface PostboyTestingSettings { strict: boolean; // default false }
  • strict: false (default): the mock auto-registers unseen message IDs with a dummy subject and unseen executors with a null-returning function. The SUT can fire and exec without prior setup.

  • strict: true: real-bus behavior. Fire or sub of an unregistered type throws, so every type the SUT touches must be registered first - via given or the registry.

const world = new PostboyWorld({ strict: true }); world.registry.recordSubject(OrderPlacedEvent); world.registry.recordExecutor(GetTaxRateExecutor, () => 0.2);

Dispose discipline

The world owns subscriptions and internal mock-namespace.../waiter-namespace... registrators. Disposal is mandatory and must happen once per test:

let world: PostboyWorld; beforeEach(() => (world = new PostboyWorld())); afterEach(() => world.dispose());

dispose() resets the history, unsubscribes the mocks, tears down both internal namespaces, and disposes the bus. The world is not reusable afterwards; do not call dispose() twice and do not touch the world after it.

Low-level escape hatches

world.mocks

Use mocks when a constant result is not enough: dynamic answers, call counters, or throwing stubs.

// Same as given.event: registers a replay subject and fires immediately world.mocks.mockEvent(new UserLoggedInEvent(42)); // The action receives the message and RETURNS the result; finish() is called for you world.mocks.mockCallback(FetchCartQuery, (m) => (m.vip ? vipCart : emptyCart)); // Executor stub with custom logic, including throwing world.mocks.mockExecute(ValidateOrderExecutor, (e) => { if (e.amount < 0) throw new Error('negative amount'); return true; });

world.registry

Direct registration on the world's namespace. In strict mode it mirrors the production registrator.

world.registry.recordSubject(OrderPlacedEvent); // plain subject - most cases world.registry.recordReplay(UserSessionEvent); // replay buffer of 1, does NOT fire world.registry.recordExecutor(GetTaxRateExecutor, () => 0.2);

Note the difference: given.event(msg) registers a replay subject for the type and fires the instance immediately, so subscribers that attach later receive it through the buffer. registry.recordReplay(Type) only registers the subject - future subscribers get the next fired value, and nothing is sent now. Use the latter for "latest value on subscribe" semantics of a future event.

Pitfalls

  • Do not register or eliminate the internal mock-namespace.../waiter-namespace... namespaces manually; that breaks dispose().

  • world.mocks.dispose() exists but is rarely needed - world.dispose() already tears everything down.

  • In strict mode a single missing registration makes the SUT throw at the first fire/sub of that type.

  • The world constructor takes only PostboyTestingSettings. Do not pass a history or a bus; the world builds its own.

Next steps

  • Assertions: the then builders and the history behind them.

  • Async waiters: the waitFor family.

  • Recipes: complete task-based tests, including a strict-mode one.

  • Registration: how registration works in production code.

07 September 2026