@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β
- Install
xstate
and@xstate/test
:
npm install xstate @xstate/test
- 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',
},
},
},
});
- 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)');
},
},
},
},
});
- 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');
},
},
});
- 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.
Argument | Type | Description |
---|---|---|
machine | StateMachine | The machine used to create the abstract model. |
options? | TestModelOptions | Options 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 dataevent
(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β
Argument | Type | Description |
---|---|---|
filter | function | Takes 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β
Argument | Type | Description |
---|---|---|
filter | function | Takes in the state and returns true if the state should be traversed, or false if traversal should stop. |
testModel.getPlanFromEvents(events, options)
β
Argument | Type | Description |
---|---|---|
events | EventObject[] | 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β
Argument | Type | Description |
---|---|---|
filter | function | Takes 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:
- Verifying that the SUT is in
segment.state
- 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.