Postboy Help

Async Testing with `waiter`

When your code emits messages inside a subscription, a timeout, or as a reaction to another async process, you can’t just check the history synchronously — the message hasn’t happened yet.

world.waiter solves this: it gives you promise‑based methods that subscribe to the future message stream and resolve when the expected message arrives (or reject if a timeout is reached).

Access it from the world:

const event = await world.waiter.waitFor(OrderPlacedEvent);

Why waiter?

Think of waiter as an async assertion.
Instead of writing flaky setTimeout calls and hoping the event arrives in time, you describe exactly what you’re waiting for.

A typical async test pattern:

// 1. Start waiting const promise = world.waiter.waitFor(TaskCompletedEvent, { where: e => e.taskId === 'abc', timeout: 2000, }); // 2. Trigger the action service.startTask('abc'); // 3. Await the event const event = await promise; expect(event.result).toBe('success');

waiter takes care of subscribing, filtering, and timing out — you stay focused on your test logic.

waiter.waitFor(type, options?) – wait for a single message

Waits for one message of the given type to be emitted on the mock bus after the call to waitFor.

const event = await world.waiter.waitFor(OrderPlacedEvent);

Options: WaitOptions<T>

interface WaitOptions<T> { timeout?: number; // default 1000 ms where?: (message: T) => boolean; includeHistory?: boolean; // default false }
  • timeout – how long to wait (ms). If omitted, the default is 1000 ms. Increase it for slower async workflows.

  • where – a predicate that filters the messages. The promise resolves only when a message passes the filter.

  • includeHistory – by default, waitFor ignores messages that were already fired. Set this to true if the message might have been sent before you called waitFor.

Examples waitFor

Ignore history (default)

service.sendAsync(); // fires inside setTimeout const event = await world.waiter.waitFor(ResponseEvent, { timeout: 1500 });

Check history (message already sent)

service.sendSync(); // fires immediately, before waitFor const event = await world.waiter.waitFor(ResponseEvent, { includeHistory: true });

With a predicate

const event = await world.waiter.waitFor(OrderEvent, { where: e => e.amount > 100 && e.userId === 42, });

If no matching message arrives within the timeout, the promise rejects with a descriptive error.

waiter.waitForMany(type, count, options?) – wait for several messages

Waits for at least count messages of the same type.

const events = await world.waiter.waitForMany(ProgressEvent, 3); expect(events).toHaveLength(3);

Options: WaitManyOptions<T>

Extends WaitOptions<T> with one extra field:

interface WaitManyOptions<T> extends WaitOptions<T> { exact?: boolean; // default false }
  • exact: false (default) – resolves as soon as at least count messages are collected.

  • exact: true – waits for exactly count messages. If more or fewer arrive by the timeout, the promise rejects.

Examples

At least 2 messages

const events = await world.waiter.waitForMany(LogEvent, 2, { timeout: 3000 });

Exactly 2 messages, no more nor fewer

const events = await world.waiter.waitForMany(LogEvent, 2, { exact: true, timeout: 2000 }); // Rejects if a third message appears before timeout, or if only one arrives.

where and includeHistory work the same as in waitFor.

waiter.waitForCallbackResult(type, options?) – wait for a callback outcome

Used with PostboyCallbackMessage<T>.
It subscribes to the callback, intercepts the finish(result) call, and returns the result as a promise.

const user = await world.waiter.waitForCallbackResult(FetchUserQuery); expect(user.name).toBe('Alice');

Under the hood, it does the same as waitFor, but instead of returning the message itself, it returns the value passed to message.finish(...).

Typical usage

const resultPromise = world.waiter.waitForCallbackResult(GetCartTotalQuery, { where: q => q.cartId === 'xyz', }); service.calculateTotal('xyz'); const total = await resultPromise; expect(total).toBe(250);

where filters the callback message before it is answered.

waiter.waitForNone(type, options?) – verify silence

Asserts that no message of the given type is fired during the specified timeout.

await world.waiter.waitForNone(ErrorOccurredEvent, { timeout: 500 });

If a matching message does appear (or is already in history when includeHistory: true), the promise rejects.

Options: WaitSilenceOptions<T>

interface WaitSilenceOptions<T> { timeout?: number; // default 1000 ms where?: (message: T) => boolean; includeHistory?: boolean; // default false timeoutMessage?: string; // custom error text }
  • timeoutMessage – a custom message for the rejection error.

await world.waiter.waitForNone(DangerousEvent, { timeoutMessage: 'DangerousEvent should NOT have been emitted', });
  • includeHistory: true – the promise will also reject if the message was already recorded in the history.

service.doSomething(); // could have fired an error await world.waiter.waitForNone(ErrorEvent, { includeHistory: true, timeout: 2000, });

This is especially useful for negative testing — “this action must not trigger an error”.

waiter.waitForAny(types[], options?) – any of multiple types

Waits for the first message that matches any of the provided constructors.

const event = await world.waiter.waitForAny([ SuccessEvent, FailureEvent, ]);

Useful when your code can branch into several outcomes and you want to inspect whichever happens first.

With options (same as WaitOptions, without where per individual type — you can filter afterwards):

const event = await world.waiter.waitForAny( [SuccessEvent, FailureEvent], { timeout: 3000, includeHistory: true } ); expect(event).toBeInstanceOf(SuccessEvent);

If you need per‑type filtering, combine with an if or expect after the promise resolves.

waiter.delay(ms) – simple time helper

A utility to pause execution. Use it sparingly, but sometimes you just need to let an async process settle.

await world.waiter.delay(100); // subscriptions have had a tick to react

Think of it as a “sleep” for your test. Prefer waitFor/waitForNone whenever a specific message is expected, as delay is not a reliable way to test async behavior.

How waiter works – internal flow

TesterTesterworld.waiterworld.waiterworld.postboyworld.postboyMessageHistoryMessageHistorywaitFor(MyEvent, options)alt[includeHistory = true]messages(MyEvent)collectionopt[message found in history]resolve with that messagesub(MyEvent).subscribe(observer)start timeout (default 1000ms)loop[until timeout or message]next(message)alt[no where or where(message) == true]unsubscriberesolve with message[predicate fails]ignore, keep waitingalt[timeout]unsubscribereject with TimeoutError

The waiter subscribes to the live message stream. If includeHistory is true, it checks the existing history once before subscribing. When a matching message is found (history or live), the promise resolves and the subscription is cleaned up. If the timeout elapses, it rejects.

Important details & caveats

  • Default timeout is 1000 ms — adjust per test case.

  • includeHistory is false by default — you must opt into checking the past.

  • waitFor, waitForMany, waitForAny only care about future messages unless includeHistory is set.

  • waiter automatically unsubscribes upon resolution or rejection, so no manual cleanup is needed inside the test.

  • Errors thrown inside where predicates will propagate to the promise.

  • waitForCallbackResult intercepts finish() — it only works with messages that actually call finish(result). If the handler never calls finish, the promise will timeout.

  • waitForNone is satisfied only when the timeout expires without a match — it’s a patience‑based check, not a proactive assertion.

Cleanup and test hygiene

Because waiter manages its own subscriptions and cleans them up when the promise settles, you don’t need to manually dispose of anything.

However, you must still call world.dispose() in afterEach to reset the mock bus and history for the next test.

afterEach(() => { world.dispose(); });

Next steps

You now have all the tools to handle async scenarios. Complement this with:

  • Given: mock setup – prepare initial events and stub callbacks/executors.

  • Then: assertions – verify what was recorded after the waiter resolves.

  • Recipes – complete async testing examples with real‑world patterns.

07 мая 2026