Stately
Guides

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

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.

On this page