Postboy Help

Best Practices

Adopting a few habits early makes your tests with @artstesh/postboy-testing more robust, readable, and maintainable.
This section distills the patterns that paying users and the library author have found most effective.

1. Always pair PostboyWorld with beforeEach/afterEach

Treat the world as a test fixture. Create it before every test and destroy it afterwards.

let world: PostboyWorld; beforeEach(() => { world = new PostboyWorld({strict: false}); }); afterEach(() => { world.dispose(); // ← non‑negotiable });

dispose() clears the history, unsubscribes all mock listeners, and removes the dedicated namespace.
Skipping it pollutes subsequent tests with stale state and left‑over subscriptions, leading to erratic failures.

2. Let the test read like a story: Arrange – Act – Assert

Structure every test in three clearly separated blocks.
postboy-testing is built around this rhythm:

it('processes an order', () => { // Arrange world.given.callback(FetchOrderQuery, order) .executor(ValidateExecutor, true); const service = new OrderProcessor(world.postboy); // Act service.process(orderId); // Assert world.then .fired(OrderProcessedEvent).once() .with(e => e.status === 'DONE'); });

This structure is instantly recognizable and guides new team members.

When the test becomes asynchronous, just lift the Assert phase into the await block.

3. Prefer given/then/waiter over raw history

The declarative APIs exist for a reason:

  • given says what was prepared.

  • then says what should have happened.

  • waiter says what should happen soon.

Raw history access is useful for edge cases (e.g., checking exact order of all messages), but for everyday tests, keep it hidden:

// Prefer this: world.then.fired(PaymentEvent).once(); // Over this: expect(world.history.messages(PaymentEvent).length).toBe(1);

The first version throws a descriptive error; the second only says “expected 1, got 0” without context.

4. Use includeHistory: true when the message may have already fired

A very common mistake:

service.run(); // fires event synchronously // ❌ The waiter will timeout because it only listens for *future* messages. await world.waiter.waitFor(MyEvent);

Fix it by allowing the waiter to inspect the history:

service.run(); await world.waiter.waitFor(MyEvent, {includeHistory: true});

Or, if you can, start the waiter before the action. Choose whichever models the real sequence better.

5. Keep mocks shallow; use given unless you need dynamic behavior

given.callback and given.executor cover most scenarios with a clean, one‑liner.

Only drop down to world.mocks.mockCallback() or mockExecute() when you truly need:

  • different answers for consecutive calls,

  • conditional logic based on the message payload,

  • throwing exceptions,

  • inspecting the raw message in the handler.

The rule of thumb: if your mock handler is longer than three lines, consider whether the production design is too complex.

6. Write negative assertions with notFired or waitForNone

Avoid manually checking history.messages(type).length === 0 — it hides the intent.

world.then.notFired(ErrorEvent); // sync check // or, if the error could appear later: await world.waiter.waitForNone(ErrorEvent, { timeout: 20, includeHistory: true });

Both clearly communicate “this must never happen” and fail with expressive messages.

7. Handle strict mode consistently

If your project enforces message registration in production, run your tests in strict mode too:

const world = new PostboyWorld({strict: true}); world.registry.recordSubject(OrderEvent); // or world.given.event(new OrderEvent(...));

This catches missing registrations early and keeps your test environment aligned with the real one.

8. Single responsibility per test

A test that triggers five different events and verifies all of them is hard to debug.
Prefer smaller, focused tests:

it('fires OrderCreated event') { /* ... */ } it('does not fire OrderFailed event for valid input') { /* ... */ } it('calls the validation executor') { /* ... */ }

A test suite composed of small, independent tests survives refactoring better than one large do‑it‑all test.

9. Use chaining in given and then to keep things concise

Chaining reduces visual noise and groups related setup/verification together:

world.given .event(new UserLoggedIn(user)) .callback(GetPermissionsQuery, permissions); // world.then .fired(ActionPerformed).once() .and() .notFired(ActionRejected);

10. Trust waiter for async; avoid setTimeout in tests

Never write:

service.doAsync(); setTimeout(() => { expect(...); done(); }, 50);

Instead:

service.doAsync(); const event = await world.waiter.waitFor(ResultEvent, {includeHistory:true}); // should().number(event.value).equals(42);

The waiter automatically subscribes, filters, times out, and cleans up after itself — no fragile timers.

11. Clean up after each test — and only after each test

Do not reuse a single PostboyWorld across multiple it blocks; unexpected interactions will appear.
Likewise, don’t call history.reset() or mocks.dispose() inside a test unless you have a specific reason. The beforeEach/afterEach pattern is sufficient and less error‑prone.

12. Keep your message constructors deterministic

Avoid random values or timestamps inside message classes used for testing.
When you need to verify a specific field, rely on the arguments passed from the test, not on side‑effects in the constructor.

// Risky in tests new OrderEvent({id: generateId(), timestamp: Date.now()}); // Good const order: Order = {id: 'order-1', timestamp: fixedDate}; new OrderEvent(order); //The best const order: Order = Forger.create<Student>()!; new OrderEvent(order);

The last approach is the most reliable and consistent, using my library forger.

At a glance: do this

  • ✅ Create world in beforeEach, dispose() in afterEach

  • ✅ Use givenactthen/waiter flow

  • ✅ Default to non‑strict for quick tests, strict for team‑wide consistency

  • ✅ Use includeHistory: true when the event may precede the waiter

  • ✅ Prefer then.fired().once() over raw history length checks

  • ✅ Chain given and then for readability

  • ✅ Validate negative scenarios with notFired and waitForNone

  • ✅ Keep tests small and descriptive

  • ✅ Trust the waiter, not setTimeout

Adopting these practices ensures that your tests remain a pleasure to write, read, and maintain as your event‑driven application grows.

07 мая 2026