How to Test Redux Reducers in React
Testing Redux reducers is a crucial step in ensuring your React application’s state transitions behave predictably. Because reducers are pure functions, writing unit tests for them is straightforward and does not require rendering React components or mocking complex network requests. This guide provides a direct, step-by-step approach to testing Redux reducers using Jest, covering initial state verification and action handling with clear code examples.
The Core Concept of Reducer Testing
A Redux reducer is a pure function that takes the current
state and an action as arguments, and returns
a new state:
(state, action) => newStateBecause of this purity, testing a reducer simply requires passing a specific state and action into the function, and asserting that the returned state matches your expectations. You do not need to set up a Redux store or use React Testing Library for these tests.
Step 1: The Reducer Under Test
Consider this simple counter reducer that manages an integer count:
// counterReducer.js
const initialState = { count: 0 };
export default function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}Step 2: Testing the Initial State
The first test should verify that the reducer returns its default
initial state when it is initialized with an undefined
state and an empty action.
import counterReducer from './counterReducer';
describe('counterReducer', () => {
it('should return the initial state when passed undefined', () => {
const result = counterReducer(undefined, { type: '@@INIT' });
expect(result).toEqual({ count: 0 });
});
});Step 3: Testing Action Handlers
Next, write test cases for each action creator or action type that the reducer handles. You provide a mock initial state, dispatch the action, and assert the output.
Testing the INCREMENT Action
it('should handle INCREMENT', () => {
const startingState = { count: 3 };
const action = { type: 'INCREMENT' };
const result = counterReducer(startingState, action);
expect(result).toEqual({ count: 4 });
});Testing the DECREMENT Action
it('should handle DECREMENT', () => {
const startingState = { count: 5 };
const action = { type: 'DECREMENT' };
const result = counterReducer(startingState, action);
expect(result).toEqual({ count: 4 });
});Testing Unhandled Actions
It is also important to verify that the reducer ignores action types it does not care about and returns the current state unchanged.
it('should return the current state for unknown action types', () => {
const startingState = { count: 10 };
const action = { type: 'SOME_UNKNOWN_ACTION' };
const result = counterReducer(startingState, action);
expect(result).toEqual({ count: 10 });
});Best Practices for Reducer Tests
- Avoid Mutation: Ensure your tests verify that the
reducer does not mutate the original state object. You can use
deep-freezing libraries like
deep-freezein your test suite to strictly enforce immutability during testing. - Test Edge Cases: Test how your reducer handles
unexpected payload values, such as
null, empty strings, or negative numbers, to prevent runtime crashes. - Keep Tests Isolated: Do not rely on the output of one test case to run the next. Each test should define its own starting state and action independently.