# @xstate/test
The @xstate/test package (opens new window) contains utilities for facilitating model-based testing (opens new window) for any software.
- Talk: Write Fewer Tests! From Automation to Autogeneration (opens new window) at React Rally 2019 (🎥 Video (opens new window))
# 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 (opens new window)):
// ...
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 executed 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.