Postboy Help

Testing: Quick Start

This page walks through one complete jest test: message classes, world setup, given stubs, service invocation, then assertions, a waiter, and teardown. Package installation is covered in Installation.

1. Install

npm install --save-dev @artstesh/postboy-testing npm install @artstesh/postboy rxjs

Both peers (@artstesh/postboy ^3.4.1, rxjs ^7) must be present.

2. Define the messages

Every message class needs a unique static ID. See Events and Executors for the message families.

// messages.ts import { PostboyGenericMessage, PostboyExecutor } from '@artstesh/postboy'; export class OrderPlacedEvent extends PostboyGenericMessage { static readonly ID = 'shop.order-placed'; constructor(public readonly orderId: number, public readonly tax: number) { super(); } } export class OrderConfirmedEvent extends PostboyGenericMessage { static readonly ID = 'shop.order-confirmed'; constructor(public readonly orderId: number) { super(); } } export class GetTaxRateExecutor extends PostboyExecutor<number> { static readonly ID = 'shop.get-tax-rate'; }

3. Write the system under test

The service depends on PostboyService; in the test it receives the mock.

// order.service.ts import { PostboyService } from '@artstesh/postboy'; export class OrderService { constructor(private readonly postboy: PostboyService) {} place(orderId: number): void { const tax = this.postboy.exec(new GetTaxRateExecutor()); // synchronous this.postboy.fire(new OrderPlacedEvent(orderId, tax)); setTimeout(() => this.postboy.fire(new OrderConfirmedEvent(orderId)), 10); } }

4. Write the test

// order.service.test.ts import { PostboyWorld } from '@artstesh/postboy-testing'; describe('OrderService', () => { let world: PostboyWorld; // One fresh world per test - never share it between tests. beforeEach(() => (world = new PostboyWorld())); // Mandatory: resets history, unsubscribes mocks, tears down namespaces. afterEach(() => world.dispose()); it('fires OrderPlacedEvent with the stubbed tax rate', () => { // Given - the executor returns a fixed rate world.given.executor(GetTaxRateExecutor, 0.2); const service = new OrderService(world.postboy); // When service.place(42); // Then - synchronous assertion on recorded history world.then .fired(OrderPlacedEvent) .once() .with((e) => e.orderId === 42 && e.tax === 0.2); }); it('confirms the order asynchronously', async () => { world.given.executor(GetTaxRateExecutor, 0.2); const service = new OrderService(world.postboy); // When - confirmation fires inside setTimeout, after this line returns service.place(42); // Then - await the future message const confirmed = await world.waiter.waitFor(OrderConfirmedEvent, { where: (e) => e.orderId === 42, }); expect(confirmed.orderId).toBe(42); }); });

What happened

  • world.given.executor(...) registered a stub, so the synchronous exec returned 0.2.

  • world.postboy recorded everything the service did through the bus.

  • world.then.fired(...) asserted the immediate fire; world.waiter.waitFor(...) awaited the delayed one.

  • world.dispose() gave the next test a clean slate.

Pitfalls

  • Waiters ignore already-recorded messages by default. If the message may have fired before the wait call, pass { includeHistory: true }.

  • world.given.event(...) fires immediately; it is a pre-fired event, not a deferred one.

  • Do not construct the service with a real PostboyService; recording only works through world.postboy.

Next steps

  • PostboyWorld: all world getters, strict mode, low-level escape hatches.

  • Assertions: the full then builder chain and history access.

  • Async waiters: every waiter method and its options.

  • Recipes: callback stubs, silence checks, strict-mode tests.

07 September 2026