Test paths
@xstate/test
generates test paths that it walks through to execute your tests. Knowing how these paths are generated will make your tests more predictable.
Coverage​
The following example models a checkbox:
ts
import { createTestMachine } from '@xstate/test';const machine = createTestMachine({initial: 'notChecked',states: {notChecked: {on: {CLICK: 'checked',},},checked: {on: {CLICK: 'notChecked',},},},});
ts
import { createTestMachine } from '@xstate/test';const machine = createTestMachine({initial: 'notChecked',states: {notChecked: {on: {CLICK: 'checked',},},checked: {on: {CLICK: 'notChecked',},},},});
You could take a few different approaches to ensure everything in this machine works:
State coverage​
The first approach is to ensure full coverage of states, where you would want to test:
- When
checked
is reached, the checkbox is displaying a checkbox. - When
notChecked
is reached, the checkbox is NOT displaying a checkbox.
Event coverage​
State coverage is a good start, but we also want to ensure all the events are working. To do this, we can add a new test:
- Ensure that
CLICK
changes the checkbox.
Putting these tests together, you’d end up with a single test path:
- Assert we’re in the
notChecked
state. - Run the
CLICK
event. - Assert we’re in the
checked
state.
Transition coverage​
The test path above feels complete, but it’s not quite there. We now know that clicking the checkbox can change it from notChecked
to checked
. But we don’t know that the same will happen when we go the other way! That means our full test should be:
- Assert we’re in the
notChecked
state. - Run the
CLICK
event. - Assert we’re in the
checked
state. - Run the
CLICK
event. - Assert we’re in the
notChecked
state.
In @xstate/test
, we achieve the test path above by checking all transitions are covered, which means you get full coverage out of the box.
Multiple paths​
Test setup can be expensive, whether you’re loading up a browser or just setting up a database. @xstate/test will speed up your tests by attempting to walk through your test model in as few paths as possible.
The following example models a login form:
ts
import { createTestMachine } from "@xstate/test";const loginMachine = createTestMachine({initial: "showingLoginForm",states: {showingLoginForm: {on: {SUBMIT_VALID_FORM: 'loggedIn',SUBMIT_INVALID_FORM: 'passwordInvalid',}},loggedIn: {}passwordInvalid: {},},});
ts
import { createTestMachine } from "@xstate/test";const loginMachine = createTestMachine({initial: "showingLoginForm",states: {showingLoginForm: {on: {SUBMIT_VALID_FORM: 'loggedIn',SUBMIT_INVALID_FORM: 'passwordInvalid',}},loggedIn: {}passwordInvalid: {},},});
This example would generate two test paths:
txt
showingLoginForm -> SUBMIT_VALID_FORM -> loggedInshowingLoginForm -> SUBMIT_INVALID_FORM -> passwordInvalid
txt
showingLoginForm -> SUBMIT_VALID_FORM -> loggedInshowingLoginForm -> SUBMIT_INVALID_FORM -> passwordInvalid
Two test paths are generated because the test model can’t transition away from the loggedIn
state or the passwordInvalid
state.
Condensing to a single path​
If we were to model the machine slightly differently, the test model would generate a single path:
ts
import { createTestMachine } from "@xstate/test";const loginMachine = createTestMachine({initial: "showingLoginForm",states: {showingLoginForm: {on: {SUBMIT_VALID_FORM: 'loggedIn',SUBMIT_INVALID_FORM: 'passwordInvalid',}},loggedIn: {on: {LOG_OUT: 'showingLoginForm'}}passwordInvalid: {},},});
ts
import { createTestMachine } from "@xstate/test";const loginMachine = createTestMachine({initial: "showingLoginForm",states: {showingLoginForm: {on: {SUBMIT_VALID_FORM: 'loggedIn',SUBMIT_INVALID_FORM: 'passwordInvalid',}},loggedIn: {on: {LOG_OUT: 'showingLoginForm'}}passwordInvalid: {},},});
In the example above, we’ve added a LOG_OUT
transition to loggedIn
, which means the test model can navigate away from the loggedIn
state. Now, the test model will run a single path:
txt
showingLoginForm-> SUBMIT_VALID_FORM -> loggedIn-> LOG_OUT -> showingLoginForm-> SUBMIT_INVALID_FORM -> passwordInvalid
txt
showingLoginForm-> SUBMIT_VALID_FORM -> loggedIn-> LOG_OUT -> showingLoginForm-> SUBMIT_INVALID_FORM -> passwordInvalid
The test above requires less setup while also testing more behavior.
Note: we don’t necessarily recommend running fewer test paths, but understanding this behavior is useful when using @xstate/test.