@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
:
bash
npm install xstate @xstate/test
bash
npm install xstate @xstate/test
- Create the machine that will be used to model the system under test (SUT):
js
import { createMachine } from 'xstate';const toggleMachine = createMachine({id: 'toggle',initial: 'inactive',states: {inactive: {on: {TOGGLE: 'active'}},active: {on: {TOGGLE: 'inactive'}}}});
js
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):
js
// ...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)');}}}}});
js
// ...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:
js
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');}}});
js
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:
js
// ...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();});});
js
// ...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:
js
const toggleModel = createModel(toggleMachine).withEvents({TOGGLE: {exec: async (page) => {await page.click('input');}}});
js
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:
js
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 todosfilter: (state) => state.context.todos.length < 5});
js
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 todosfilter: (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. |
js
// Only test coverage for state nodes with a `.meta` property defined:testModel.testCoverage({filter: (stateNode) => !!stateNode.meta});
js
// 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.