Testing
How to test state machine and actor logic in XState
Testing logic
Testing actor logic is important for ensuring that the logic is correct and that it behaves as expected. You can test your state machines and actors using various testing libraries and tools. You should follow the Arrange, Act, Assert pattern when writing tests for your state machines and actors:
- Arrange - set up the test by creating the actor logics (such as a state machine) and the actors from the actor logics.
- Act - send event(s) to the actor(s).
- Assert - assert that the actor(s) reached their expected state(s) and/or executed the expected side effects.
import { setup, createActor } from 'xstate';
import { test, expect } from 'vitest';
test('some actor', async () => {
const notifiedMessages: string[] = [];
// 1. Arrange
const machine = setup({
actions: {
notify: (_, params) => {
notifiedMessages.push(params.message);
},
},
}).createMachine({
initial: 'inactive',
states: {
inactive: {
on: { toggle: { target: 'active' } },
},
active: {
entry: { type: 'notify', params: { message: 'Active!' } },
on: { toggle: { target: 'inactive' } },
},
},
});
const actor = createActor(machine);
// 2. Act
actor.start();
actor.send({ type: 'toggle' }); // => should be in 'active' state
actor.send({ type: 'toggle' }); // => should be in 'inactive' state
actor.send({ type: 'toggle' }); // => should be in 'active' state
// 3. Assert
expect(actor.getSnapshot().value).toBe('active');
expect(notifiedMessages).toEqual(['Active!', 'Active!']);
});You can now generate test paths from your state machines in Stately Studio. You can try Stately Studio’s premium plans with a free trial. Check out the features on our Pro plan, Team plan, Enterprise plan or upgrade your existing plan.
Testing actors
When testing actors, you typically want to verify that they transition to the correct state and update their context appropriately when receiving events.
import { setup, createActor } from 'xstate';
import { test, expect } from 'vitest';
test('actor transitions correctly', () => {
const toggleMachine = setup({}).createMachine({
initial: 'inactive',
context: { count: 0 },
states: {
inactive: {
on: {
activate: {
target: 'active',
actions: assign({ count: ({ context }) => context.count + 1 })
}
}
},
active: {
on: {
deactivate: 'inactive'
}
}
}
});
const actor = createActor(toggleMachine);
actor.start();
// Test initial state
expect(actor.getSnapshot().value).toBe('inactive');
expect(actor.getSnapshot().context.count).toBe(0);
// Send event and test transition
actor.send({ type: 'activate' });
expect(actor.getSnapshot().value).toBe('active');
expect(actor.getSnapshot().context.count).toBe(1);
// Send another event
actor.send({ type: 'deactivate' });
expect(actor.getSnapshot().value).toBe('inactive');
expect(actor.getSnapshot().context.count).toBe(1);
});Mocking effects
When testing state machines that have side effects (like API calls, logging, or other external interactions), you should mock these effects to make your tests deterministic and isolated.
import { setup, createActor } from 'xstate';
import { test, expect, vi } from 'vitest';
test('mocking actions', () => {
const mockLogger = vi.fn();
const machine = setup({
actions: {
// Mock the logging action
logMessage: mockLogger
}
}).createMachine({
initial: 'idle',
states: {
idle: {
on: {
start: {
target: 'running',
actions: {
type: 'logMessage',
params: { message: 'Started!' }
}
}
}
},
running: {}
}
});
const actor = createActor(machine);
actor.start();
actor.send({ type: 'start' });
expect(actor.getSnapshot().value).toBe('running');
expect(mockLogger).toHaveBeenCalledWith(
expect.anything(), // action meta
{ message: 'Started!' } // params
);
});For promise-based actors, you can mock the promises:
test('mocking promise actors', async () => {
const mockFetch = vi.fn().mockResolvedValue({ data: 'test' });
const machine = setup({
actors: {
fetchData: fromPromise(mockFetch)
}
}).createMachine({
initial: 'idle',
states: {
idle: {
on: {
fetch: 'loading'
}
},
loading: {
invoke: {
src: 'fetchData',
onDone: 'success',
onError: 'error'
}
},
success: {},
error: {}
}
});
const actor = createActor(machine);
actor.start();
actor.send({ type: 'fetch' });
// Wait for promise to resolve
await new Promise(resolve => setTimeout(resolve, 0));
expect(actor.getSnapshot().value).toBe('success');
expect(mockFetch).toHaveBeenCalled();
});Using @xstate/test
The XState Test model-based testing utilities have moved into xstate itself and are now available under xstate/graph. The standalone @xstate/test package is deprecated in favor of the integrated testing utilities.
The model-based testing utilities allow you to automatically generate test cases from your state machines, ensuring comprehensive coverage of all possible paths and edge cases.