Skip to content
Version: XState v5

@xstate/test

The @xstate/test package contains utilities for facilitating model-based testing for any software.

Watch the talk: Write Fewer Tests! From Automation to Autogeneration at React Rally 2019 (πŸŽ₯ Video)

Quick start​

  1. Install xstate and @xstate/test:
npm install xstate @xstate/test
  1. Create the machine that will be used to model the system under test (SUT):
import { createMachine } from 'xstate';

const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: {
TOGGLE: 'active',
},
},
active: {
on: {
TOGGLE: 'inactive',
},
},
},
});
  1. Add assertions for each state in the machine (in this example, using Puppeteer):
// ...

const toggleMachine = createMachine({
id: 'toggle',
initial: 'inactive',
states: {
inactive: {
on: {
/* ... */
},
meta: {
test: async (page) => {
await page.waitFor('input:checked');
},
},
},
active: {
on: {
/* ... */
},
meta: {
test: async (page) => {
await page.waitFor('input:not(:checked)');
},
},
},
},
});
  1. Create the model:
import { createMachine } from 'xstate';
import { createModel } from '@xstate/test';

const toggleMachine = createMachine(/* ... */);

const toggleModel = createModel(toggleMachine).withEvents({
TOGGLE: {
exec: async (page) => {
await page.click('input');
},
},
});
  1. Create test plans and run the tests with coverage:
// ...

describe('toggle', () => {
const testPlans = toggleModel.getShortestPathPlans();

testPlans.forEach((plan) => {
describe(plan.description, () => {
plan.paths.forEach((path) => {
it(path.description, async () => {
// do any setup, then...

await path.test(page);
});
});
});
});

it('should have full coverage', () => {
return toggleModel.testCoverage();
});
});

API​

createModel(machine, options?)​

Creates an abstract testing model based on the machine passed in.

ArgumentTypeDescription
machineStateMachineThe machine used to create the abstract model.
options?TestModelOptionsOptions to customize the abstract model

Returns​

A TestModel instance.

Methods​

model.withEvents(eventsMap)​

Provides testing details for each event. Each key in eventsMap is an object whose keys are event types and properties describe the execution and test cases for each event:

  • exec (function): Function that executes the events. It is given two arguments:
    • testContext (any): any contextual testing data
    • event (EventObject): the event sent by the testing model
  • cases? (EventObject[]): the sample event objects for this event type that can be sent by the testing model.

Example:

const toggleModel = createModel(toggleMachine).withEvents({
TOGGLE: {
exec: async (page) => {
await page.click('input');
},
},
});

testModel.getShortestPathPlans(options?)​

Returns an array of testing plans based on the shortest paths from the test model’s initial state to every other reachable state.

Options​

ArgumentTypeDescription
filterfunctionTakes in the state and returns true if the state should be traversed, or false if traversal should stop.

This is useful for preventing infinite traversals and stack overflow errors:

const todosModel = createModel(todosMachine).withEvents({
/* ... */
});

const plans = todosModel.getShortestPathPlans({
// Tell the algorithm to limit state/event adjacency map to states
// that have less than 5 todos
filter: (state) => state.context.todos.length < 5,
});

testModel.getSimplePathPlans(options?)​

Returns an array of testing plans based on the simple paths from the test model’s initial state to every other reachable state.

Options​

ArgumentTypeDescription
filterfunctionTakes in the state and returns true if the state should be traversed, or false if traversal should stop.

testModel.getPlanFromEvents(events, options)​

ArgumentTypeDescription
eventsEventObject[]The sequence of events to create the plan
options{ target: string }An object with a target property that should match the target state of the events

Returns an array with a single testing plan with a single path generated from the events.

Throws an error if the last entered state does not match the options.target.

testModel.testCoverage(options?)​

Tests that all state nodes were covered (traversed) in the exected tests.

Options​

ArgumentTypeDescription
filterfunctionTakes in each stateNode and returns true if that state node should have been covered.
// Only test coverage for state nodes with a `.meta` property defined:

testModel.testCoverage({
filter: (stateNode) => !!stateNode.meta,
});

testPlan.description​

The string description of the testing plan, describing the goal of reaching the testPlan.state.

testPlan.paths​

The testing paths to get from the test model’s initial state to every other reachable state.

testPath.description​

The string description of the testing path, describing a sequence of events that will reach the testPath.state.

testPath.test(testContext)​

Executes each step in testPath.segments by:

  1. Verifying that the SUT is in segment.state
  2. Executing the event for segment.event

And finally, verifying that the SUT is in the target testPath.state.

NOTE: If your model has nested states, the meta.test method for each parent state of that nested state is also executed when verifying that the SUT is in that nested state.