How to Test Redux Actions in React

Testing Redux actions is a crucial step in ensuring your React application’s state management behaves predictably. This article provides a straightforward guide on how to test both synchronous and asynchronous Redux actions using Jest and a mock Redux store. You will learn how to set up your test suite, write assertions for basic action creators, and handle asynchronous side effects like API calls.

Testing Synchronous Actions

Synchronous action creators in Redux are pure functions that return a plain JavaScript object. Because they have no side effects, testing them is highly straightforward. You only need to call the action creator with specific arguments and assert that it returns the expected action object.

Here is an example of a synchronous action creator:

// actions.js
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  payload: text,
});

To test this action creator using Jest, write the following test case:

// actions.test.js
import { addTodo } from './actions';

describe('actions', () => {
  it('should create an action to add a todo', () => {
    const text = 'Finish writing test';
    const expectedAction = {
      type: 'ADD_TODO',
      payload: text,
    };
    expect(addTodo(text)).toEqual(expectedAction);
  });
});

Testing Asynchronous Actions (Thunks)

Asynchronous action creators, often created using middleware like redux-thunk, involve side effects such as fetching data from an API. Testing these requires mocking both the Redux store and the HTTP requests.

To test thunks, you need to install redux-mock-store to capture dispatched actions, and mock your network requests using Jest’s built-in mocking utilities.

Here is an example of an asynchronous action creator:

// asyncActions.js
export const fetchUsersSuccess = (users) => ({
  type: 'FETCH_USERS_SUCCESS',
  payload: users,
});

export const fetchUsers = () => {
  return (dispatch) => {
    return fetch('/api/users')
      .then((response) => response.json())
      .then((users) => dispatch(fetchUsersSuccess(users)));
  };
};

To test this asynchronous action, configure a mock store with the redux-thunk middleware and mock the global fetch API:

// asyncActions.test.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fetchUsers } from './asyncActions';

const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);

describe('async actions', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('creates FETCH_USERS_SUCCESS when fetching users has been done', () => {
    const mockUsers = [{ id: 1, name: 'John Doe' }];
    
    // Mock the fetch API response
    global.fetch.mockResolvedValueOnce({
      json: () => Promise.resolve(mockUsers),
    });

    const expectedActions = [
      { type: 'FETCH_USERS_SUCCESS', payload: mockUsers }
    ];

    const store = mockStore({ users: [] });

    return store.dispatch(fetchUsers()).then(() => {
      expect(store.getActions()).toEqual(expectedActions);
    });
  });
});

By verifying that your synchronous action creators return the correct objects and that your asynchronous thunks dispatch the expected sequence of actions, you can guarantee the integrity of your React-Redux application’s data flow.