Skip to content
Version: XState v5

Testing

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!']);
});

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 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.