# Testing Machines
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 });
});