How to Test Redux Middleware in React
Testing Redux middleware is essential for ensuring that side effects, API calls, and custom action transformations behave predictably in your React applications. This article provides a straightforward guide on how to test Redux middleware, covering both isolated unit testing of the middleware function and integration testing using a mock Redux store.
Understanding the Middleware Signature
To test Redux middleware, you must first understand its curried signature. Redux middleware is structured as three nested functions:
const middleware = store => next => action => { /* ... */ };store: A mocked version of the Redux store containinggetStateanddispatch.next: The next dispatch function in the middleware chain.action: The action currently being dispatched.
By exploiting this structure, you can test middleware either by invoking these nested functions manually (isolated unit testing) or by using a mock store library (integration testing).
Method 1: Isolated Unit Testing (No External Libraries)
Isolated unit testing is the cleanest way to test middleware because
it does not require configuring a real or mock Redux store. You simply
mock the store, next, and action
arguments.
The Middleware to Test
Here is a simple middleware that adds a timestamp payload to specific actions:
// timestampMiddleware.js
export const timestampMiddleware = store => next => action => {
if (action.type === 'ADD_TODO') {
const actionWithTimestamp = {
...action,
meta: { ...action.meta, timestamp: Date.now() }
};
return next(actionWithTimestamp);
}
return next(action);
};The Unit Test
Using Jest, you can create mock functions for store and
next to assert that the middleware behaves correctly.
// timestampMiddleware.test.js
import { timestampMiddleware } from './timestampMiddleware';
describe('timestampMiddleware', () => {
let mockStore;
let mockNext;
beforeEach(() => {
mockStore = {
getState: jest.fn(() => ({})),
dispatch: jest.fn()
};
mockNext = jest.fn();
});
it('should pass unmodified action if type is not ADD_TODO', () => {
const action = { type: 'SOME_OTHER_ACTION' };
// Invoke the curried middleware function
timestampMiddleware(mockStore)(mockNext)(action);
expect(mockNext).toHaveBeenCalledWith(action);
});
it('should add a timestamp to ADD_TODO actions', () => {
const action = { type: 'ADD_TODO', payload: 'Buy milk' };
// Freeze time to test the timestamp accurately
const mockTime = 1600000000000;
jest.spyOn(Date, 'now').mockImplementation(() => mockTime);
timestampMiddleware(mockStore)(mockNext)(action);
expect(mockNext).toHaveBeenCalledWith({
type: 'ADD_TODO',
payload: 'Buy milk',
meta: { timestamp: mockTime }
});
jest.restoreAllMocks();
});
});Method 2:
Integration Testing with redux-mock-store
If your middleware dispatches new actions or interacts heavily with the store’s state, testing with a mock store provides a more realistic test environment.
The Middleware to Test
Consider a middleware that fetches data from an API when a specific action is dispatched:
// apiMiddleware.js
export const apiMiddleware = store => next => async action => {
if (action.type === 'FETCH_USER_REQUEST') {
next(action); // Pass the request action along
try {
const response = await fetch(`/api/user/${action.payload}`);
const data = await response.json();
return store.dispatch({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (error) {
return store.dispatch({ type: 'FETCH_USER_FAILURE', error: error.message });
}
}
return next(action);
};The Integration Test
Using redux-mock-store, you can configure a mock store
with your middleware and assert against the array of actions dispatched
during the test execution.
// apiMiddleware.test.js
import configureMockStore from 'redux-mock-store';
import { apiMiddleware } from './apiMiddleware';
// Recreate the mock store creator with the middleware applied
const mockStoreCreator = configureMockStore([apiMiddleware]);
describe('apiMiddleware', () => {
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
it('dispatches FETCH_USER_SUCCESS on successful API call', async () => {
const mockUser = { id: 1, name: 'John Doe' };
// Mock the global fetch implementation
global.fetch.mockResolvedValueOnce({
json: async () => mockUser,
});
const store = mockStoreCreator({});
// Dispatch the trigger action
await store.dispatch({ type: 'FETCH_USER_REQUEST', payload: 1 });
const actions = store.getActions();
// Expect the initial request and the success action to have been dispatched
expect(actions[0]).toEqual({ type: 'FETCH_USER_REQUEST', payload: 1 });
expect(actions[1]).toEqual({ type: 'FETCH_USER_SUCCESS', payload: mockUser });
});
it('dispatches FETCH_USER_FAILURE on failed API call', async () => {
global.fetch.mockRejectedValueOnce(new Error('Network Error'));
const store = mockStoreCreator({});
await store.dispatch({ type: 'FETCH_USER_REQUEST', payload: 1 });
const actions = store.getActions();
expect(actions[0]).toEqual({ type: 'FETCH_USER_REQUEST', payload: 1 });
expect(actions[1]).toEqual({ type: 'FETCH_USER_FAILURE', error: 'Network Error' });
});
});