# Testing Machines
These XState v4 docs are no longer maintained
XState v5 is out now! Read more about XState v5 (opens new window)
🆕 Find more about testing using XState (opens new window) in our new docs.
In general, testing state machines and statecharts should be done by testing the overall behavior of the machine; that is:
Given a current state, when some sequence of events occurs, the system under test should be in a certain state and/or exhibit a specific output.
This follows behavior-driven development (BDD) (opens new window) and black-box testing (opens new window) strategies. The internal workings of a machine should not be directly tested; rather, the observed behavior should be tested instead. This makes testing machines closer to integration or end-to-end (E2E) tests than unit tests.
# Testing pure logic
If you do not want to test side-effects, such as executing actions or invoking actors, and want to instead test pure logic, the machine.transition(...)
function can be used to assert that a specific state is reached given an initial state and an event:
import { lightMachine } from '../path/to/lightMachine';
it('should reach "yellow" given "green" when the "TIMER" event occurs', () => {
const expectedValue = 'yellow'; // the expected state value
const actualState = lightMachine.transition('green', { type: 'TIMER' });
expect(actualState.matches(expectedValue)).toBeTruthy();
});
# Testing services
The behavior and output of services can be tested by asserting that it eventually reaches an expected state, given an initial state and a sequence of events:
import { fetchMachine } from '../path/to/fetchMachine';
it('should eventually reach "success"', (done) => {
const fetchService = interpret(fetchMachine).onTransition((state) => {
// this is where you expect the state to eventually
// be reached
if (state.matches('success')) {
done();
}
});
fetchService.start();
// send zero or more events to the service that should
// cause it to eventually reach its expected state
fetchService.send({ type: 'FETCH', id: 42 });
});
TIP
Keep in mind that most testing frameworks have a default timeout, and the async tests are expected to finish before that timeout. Configure the timeout if necessary (e.g., jest.setTimeout(timeout)
(opens new window)) for longer-running tests.
# Mocking effects
Since actions and invoking/spawning actors are side-effects, it might be undesirable to execute them in a testing environment. You can use the machine.withConfig(...)
option to change the implementation details of certain actions:
import { fetchMachine } from '../path/to/fetchMachine';
it('should eventually reach "success"', (done) => {
let userAlerted = false;
const mockFetchMachine = fetchMachine.withConfig({
services: {
fetchFromAPI: (_, event) =>
new Promise((resolve) => {
setTimeout(() => {
resolve({ id: event.id });
}, 50);
})
},
actions: {
alertUser: () => {
// set a flag instead of executing the original action
userAlerted = true;
}
}
});
const fetchService = interpret(mockFetchMachine).onTransition((state) => {
if (state.matches('success')) {
// assert that effects were executed
expect(userAlerted).toBeTruthy();
done();
}
});
fetchService.start();
fetchService.send({ type: 'FETCH', id: 42 });
});