How to Mock Redux Thunk in React

Testing asynchronous actions in React applications often requires mocking Redux Thunk to isolate component behavior and verify state changes. This article provides a straightforward, step-by-step guide on how to mock Redux Thunk using Jest and the redux-mock-store library, enabling you to write clean, reliable unit tests for your asynchronous action creators.

Prerequisites

To mock Redux Thunk, you will need to install redux-mock-store. This library allows you to test Redux action creators and middleware without relying on the actual Redux store.

Run the following command in your terminal:

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

Step 1: Configure the Mock Store

To test thunks, you must apply the redux-thunk middleware to your mock store. This ensures that when you dispatch an asynchronous action, the mock store knows how to process the function returned by the thunk.

Create your mock store configuration like this:

import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';

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

Step 2: Create the Thunk Action to Test

Assume you have an asynchronous action creator (thunk) that fetches data from an API and dispatches success or failure actions:

// actions.js
export const fetchUserData = (userId) => 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', error: error.message });
  }
};

Step 3: Write the Unit Test

To test the thunk, mock the global fetch API (or your HTTP client, such as Axios), dispatch the thunk action to the mock store, and assert that the expected actions were dispatched in the correct order.

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

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

describe('fetchUserData Thunk', () => {
  beforeEach(() => {
    // Reset fetch mocks before each test
    global.fetch = jest.fn();
  });

  it('dispatches FETCH_USER_REQUEST and FETCH_USER_SUCCESS on success', async () => {
    const mockUser = { id: '123', name: 'John Doe' };
    
    // Mock successful API response
    global.fetch.mockResolvedValueOnce({
      json: jest.fn().mockResolvedValueOnce(mockUser),
    });

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

    const store = mockStore({});

    // Dispatch the thunk and wait for it to resolve
    await store.dispatch(fetchUserData('123'));

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

  it('dispatches FETCH_USER_REQUEST and FETCH_USER_FAILURE on error', async () => {
    // Mock API error
    global.fetch.mockRejectedValueOnce(new Error('Network Error'));

    const expectedActions = [
      { type: 'FETCH_USER_REQUEST' },
      { type: 'FETCH_USER_FAILURE', error: 'Network Error' }
    ];

    const store = mockStore({});

    await store.dispatch(fetchUserData('123'));

    expect(store.getActions()).toEqual(expectedActions);
  });
});

Conclusion

By combining redux-mock-store with redux-thunk middleware, you can easily execute and test asynchronous flows. This approach allows you to inspect the list of dispatched actions and assert that your application behaves correctly during both successful and failed network requests.