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:
Inside, it assembles six cooperating pieces:
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.
Arrange – use
world.givento prepare the mock environment and injectworld.postboyinto your code.Act – call the method you’re testing; the mock bus records every interaction.
Assert – verify the recorded history with
world.thenor wait for asynchronous outcomes withworld.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:
Adds an entry to the internal
MessageHistory.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:
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 withresult.given.executor(Type, result)– when an executor is invoked, the mock returnsresult.
All these methods return this, so you can chain them:
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),.valuethen.notFired(Type)→ assert a message was never sentthen.subscribed(Type)→ verify subscription counts.once(),.times(n),.atLeast(n)
You can chain multiple checks together with .and():
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:
8. Strict vs. non‑strict mode
The world can run in two modes, controlled by the PostboyTestingSettings option.
Mode | Setting | Behavior |
|---|---|---|
Non‑strict (default) |
| No pre‑registration required. Messages work out of the box. |
Strict |
| Every message type must be explicitly registered via |
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:
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:
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:
Then: full assertion API – all verification methods in detail.
Given: mock setup – learn how to mock callbacks, executors, and pre‑sent events.
Waiter: async testing –
waitForMany,waitForNone,waitForCallbackResultand best practices.Strict mode & registry – when and how to use explicit registration.
History deep‑dive – working directly with
MessageHistoryandHistoryCollection.
You’re holding a complete testing toolkit. The rest of the documentation will turn you into an expert.