How to Test Redux Thunk in React

Testing Redux Thunk in React is essential for ensuring your asynchronous logic, such as API requests, works correctly and dispatches the expected actions. This article provides a clear, step-by-step guide on how to unit test Redux Thunks. You will learn how to set up mock stores, mock external API calls, and assert that the correct sequence of actions is dispatched using popular testing tools like Jest and Redux Mock Store.

Setting Up the Tools

To test Redux Thunks, you need a test runner (such as Jest or Vitest) and a way to mock the Redux store. The industry standard library for this is redux-mock-store. Because thunks perform asynchronous operations, you also need to mock network requests.

Install the required dependencies:

npm install --save-dev redux-mock-store redux-thunk

The Thunk to Test

Consider a standard asynchronous action creator that fetches user data from an API:

// userActions.js
export const fetchUser = (userId) => {
  return async (dispatch) => {
    dispatch({ type: 'FETCH_USER_REQUEST' });
    try {
      const response = await fetch(`/api/user/${userId}`);
      const data = await response.json();
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'FETCH_USER_FAILURE', payload: error.message });
    }
  };
};

Writing the Unit Test

To test this thunk, configure redux-mock-store with the redux-thunk middleware. Then, mock the global fetch API to simulate both successful and failed network responses.

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

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

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

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

  it('dispatches SUCCESS action after a successful API call', async () => {
    const mockUserData = { id: 1, name: 'John Doe' };
    
    // Mock successful fetch response
    global.fetch.mockResolvedValueOnce({
      json: async () => mockUserData,
    });

    const expectedActions = [
      { type: 'FETCH_USER_REQUEST' },
      { type: 'FETCH_USER_SUCCESS', payload: mockUserData }
    ];

    const store = mockStore({ user: {} });

    // Execute the thunk
    await store.dispatch(fetchUser(1));

    // Verify the sequence of dispatched actions
    expect(store.getActions()).toEqual(expectedActions);
  });

  it('dispatches FAILURE action when the API call fails', async () => {
    const errorMessage = 'Network Error';
    
    // Mock fetch rejection
    global.fetch.mockRejectedValueOnce(new Error(errorMessage));

    const expectedActions = [
      { type: 'FETCH_USER_REQUEST' },
      { type: 'FETCH_USER_FAILURE', payload: errorMessage }
    ];

    const store = mockStore({ user: {} });

    // Execute the thunk
    await store.dispatch(fetchUser(1));

    // Verify the sequence of dispatched actions
    expect(store.getActions()).toEqual(expectedActions);
  });
});

Testing with Redux Toolkit (RTK)

If you are using modern Redux Toolkit (createAsyncThunk), you do not need redux-mock-store. Instead, you can test the thunk directly by mocking the dispatch and getState functions.

// userSlice.test.js
import { fetchUserByThunk } from './userSlice'; // Thunk created via createAsyncThunk

describe('fetchUserByThunk', () => {
  it('should dispatch pending and fulfilled actions', async () => {
    const dispatch = jest.fn();
    const getState = jest.fn();
    const userData = { id: 1, name: 'John Doe' };

    global.fetch = jest.fn().mockResolvedValueOnce({
      json: async () => userData,
    });

    const thunk = fetchUserByThunk(1);
    await thunk(dispatch, getState, undefined);

    const { calls } = dispatch.mock;
    
    // Assert the pending action was dispatched first
    expect(calls[0][0].type).toBe('user/fetchById/pending');
    
    // Assert the fulfilled action was dispatched second with the correct payload
    expect(calls[1][0].type).toBe('user/fetchById/fulfilled');
    expect(calls[1][0].payload).toEqual(userData);
  });
});