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-thunkThe 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);
});
});