Use simulate for a complete event list:
const result = yield * simulate(machine, events, { input });
expect(result.states.map((state) => state._tag)).toEqual(["Idle", "Loading", "Done"]);Use createTestHarness when each step needs an assertion. Use assertPath, assertReaches, and assertNeverReaches for invariant-style tests.
Simulation and replay run pure or Effectful transition handlers. They do not run tasks, state-owned Effects, background Effects, or timeouts. Their self operations are inert.
Machine.definition(machine) returns a frozen snapshot of every registration the machine holds. It reads the indexes the runtime selects from, so it cannot drift from runtime behavior.
states: one entry per state, in schema order, including states with no registration. Each entry hastransitions(in the order the runtime tries them),immediate,effects(spawn, task and timeout, in start order),postponedandfinal.events: the event tags, in schema order.anyState: the.onAny/.whenAnytransitions. The runtime tries them after the state's own transitions for the same event.background: the machine-lifetime Effects.
Each entry reports the builder method and names: handlerName and guardName come from the function name, a task reports options.name, a spawn reports options.name or else the handler's function name, and a timeout reports $timeout:<StateTag>. An anonymous function reports undefined. An Effect.fn result is also anonymous: its span name is not a function name. Give an Effect.fn spawn handler a name with the third argument:
machine.spawn(State.Paying, Effect.fn("Checkout.watchTerminal")(watchTerminal), {
name: "watchTerminal",
});When a test must identify a transition handler, use a function declaration, or a plain function or arrow assigned to a const.
The definition reports registrations, not targets. Handlers compute targets at run time. Use Machine.replay from a state to find the target of a transition.
Use it for a coverage test of state-owned event transitions: every registered state and event pair has a case, and every case names a registration. Check anyState, immediate and effects the same way when the test must cover them.
// One case per state/event pair: the events to replay from that state.
const cases = {
"Idle/Start": [MyEvent.Start],
"Loading/Done": [MyEvent.Done],
};
const registered = Machine.definition(machine).states.flatMap((state) =>
state.transitions.map((transition) => `${state.tag}/${transition.event}`),
);
expect([...new Set(registered)].sort()).toStrictEqual(Object.keys(cases).sort());Call it again after more builder calls. A snapshot does not change.
Spawn a real actor when a test covers services, tasks, child actors, persistence, supervision, inspection, or cleanup.
Provide test services through Effect:
it.scopedLive("loads the order", () => program.pipe(Effect.provide(OrderRepositoryTest)));Use TestClock for timeouts and schedules. Use Deferred, Latch, Queue, Ref, or a test service hook for concurrent ordering. Do not use an arbitrary sleep to guess that work has started.
Test the public rendering behavior:
- Count component renders in React.
- Count selected reactive computations in Solid.
- Change one field at a time.
- Assert that unrelated selections do not update.
- Assert that the changed selection updates once.
The repository runs these checks in both browser example packages.