Skip to content

Redux - Testing

Testing Redux applications is generally straightforward because Redux relies heavily on pure functions, especially for reducers. Actions are typically simple objects, and action creators are often simple functions returning those objects. This makes unit testing easy without extensive mocking.

We’ll use Jest as our testing framework. Jest is a popular JavaScript testing framework that works well with Node.js environments and doesn’t require a DOM for most Redux testing.

If you haven’t already, install Jest:

npm install --save-dev jest
# or
yarn add --dev jest
# or
pnpm add --save-dev jest

If you’re using Babel or TypeScript, ensure Jest is configured correctly. For Babel, you might need babel-jest:

npm install --save-dev babel-jest @babel/core @babel/preset-env
# or
yarn add --dev babel-jest @babel/core @babel/preset-env
# or
pnpm add --save-dev babel-jest @babel/core @babel/preset-env

Configure Babel via babel.config.js or .babelrc. A simple configuration might look like:

// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {targets: {node: 'current'}}],
// Add other presets like @babel/preset-react or @babel/preset-typescript if needed
],
};

Add a test script to your package.json:

{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
}

Run tests with npm test or yarn test or pnpm test.

Action creators are functions that return action objects. Testing them involves calling the function and asserting that the returned object matches the expected action.

Consider this simple action creator:

// actions/itemActions.js
export const ITEMS_REQUEST_SUCCESS = 'items/requestSuccess';
export const itemsRequestSuccess = (isLoading) => ({
type: ITEMS_REQUEST_SUCCESS,
payload: { isLoading },
});

A test for this action creator would look like this:

// actions/itemActions.test.js
import { itemsRequestSuccess, ITEMS_REQUEST_SUCCESS } from './itemActions';
describe('item actions', () => {
it('itemsRequestSuccess should create an action with isLoading payload', () => {
const isLoading = false;
const expectedAction = {
type: ITEMS_REQUEST_SUCCESS,
payload: { isLoading },
};
expect(itemsRequestSuccess(isLoading)).toEqual(expectedAction);
});
});

If you’re using Redux Toolkit’s createSlice, action creators are generated automatically. You typically test their effects through reducer tests or integration tests.

Reducers must be pure functions: given the same state and action, they must always return the same new state without side effects or mutations.

Consider a simple reducer (perhaps part of a slice generated by Redux Toolkit):

// reducers/itemReducer.js
const initialState = {
isLoading: false,
items: [],
};
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case 'items/requestStart':
return {
...state,
isLoading: true,
};
case 'items/requestSuccess':
return {
...state,
isLoading: action.payload.isLoading, // Assuming payload: { isLoading: boolean }
};
// Example with items payload
case 'items/set':
return {
...state,
items: action.payload, // Assuming payload is an array of items
};
default:
return state;
}
};
export default itemReducer;

To test this reducer, provide an initial state, dispatch an action, and assert that the returned state is correct:

// reducers/itemReducer.test.js
import itemReducer from './itemReducer';
describe('itemReducer', () => {
const initialState = {
isLoading: false,
items: [],
};
it('should return the initial state if no state is provided', () => {
expect(itemReducer(undefined, {})).toEqual(initialState);
});
it('should handle items/requestStart action', () => {
const action = { type: 'items/requestStart' };
const expectedState = {
...initialState,
isLoading: true,
};
expect(itemReducer(initialState, action)).toEqual(expectedState);
});
it('should handle items/requestSuccess action', () => {
const previousState = { ...initialState, isLoading: true };
const action = {
type: 'items/requestSuccess',
payload: { isLoading: false },
};
const expectedState = {
...initialState,
isLoading: false,
};
expect(itemReducer(previousState, action)).toEqual(expectedState);
});
it('should handle items/set action', () => {
const action = {
type: 'items/set',
payload: [{ id: 1, name: 'Test Item' }],
};
const expectedState = {
...initialState,
items: [{ id: 1, name: 'Test Item' }],
};
expect(itemReducer(initialState, action)).toEqual(expectedState);
});
});

Testing asynchronous action creators (like thunks created with createAsyncThunk or manually) often involves mocking API calls and checking which actions are dispatched.

Libraries like redux-mock-store can be helpful, or you can test the thunk directly by mocking dispatch and getState.

Example using Jest mocks (conceptual):

// actions/asyncActions.test.js
import { createAsyncThunk } from '@reduxjs/toolkit';
import * as api from '../api'; // Your API call module
// Assume fetchItems is an async thunk
// const fetchItems = createAsyncThunk('items/fetch', async () => { ... });
jest.mock('../api'); // Mock the API module
describe('async actions', () => {
it('fetchItems dispatches pending and fulfilled actions on success', async () => {
const mockDispatch = jest.fn();
const mockGetState = jest.fn();
const mockData = [{ id: 1, name: 'Fetched Item' }];
api.fetchItemsFromApi.mockResolvedValue(mockData); // Mock the API call
// Assume fetchItems is your async thunk function
// await fetchItems()(mockDispatch, mockGetState, undefined);
// Assertions would depend on how fetchItems is implemented (e.g., using createAsyncThunk)
// Example: Check if pending action was dispatched
// expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'items/fetch/pending' }));
// Example: Check if fulfilled action was dispatched with correct payload
// expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining({
// type: 'items/fetch/fulfilled',
// payload: mockData,
// }));
});
});

For more complex scenarios or integration testing, consider using libraries like Testing Library with a real Redux store.

For more detailed information, refer to the official documentation: