Testing Redux in React: A Complete Guide

This article provides a straightforward guide on how to test Redux in React applications. You will learn how to write unit tests for Redux reducers, action creators, and selectors, as well as how to perform integration testing on React components connected to a Redux store using Jest and React Testing Library.

Testing Reducers

Reducers are pure functions that take the current state and an action as arguments and return a new state. Because they are pure, they are the easiest part of a Redux setup to unit test. You do not need to mock anything; simply pass an initial state and an action, then assert that the output matches your expectations.

import counterReducer from './counterSlice';

test('should return the initial state when passed an empty action', () => {
  expect(counterReducer(undefined, { type: undefined })).toEqual({ value: 0 });
});

test('should handle increment action', () => {
  const previousState = { value: 1 };
  expect(counterReducer(previousState, { type: 'counter/increment' })).toEqual({ value: 2 });
});

Testing Action Creators and Selectors

Action creators and selectors are also pure functions. Testing them involves verifying their inputs and outputs.

For action creators, assert that the function returns the correct action object containing the expected type and payload:

import { incrementByAmount } from './counterSlice';

test('should create an action to increment by a specific amount', () => {
  const expectedAction = { type: 'counter/incrementByAmount', payload: 5 };
  expect(incrementByAmount(5)).toEqual(expectedAction);
});

For selectors, pass a mock state object to the selector function and assert that it extracts the correct slice of data:

import { selectCount } from './counterSelectors';

test('should select the count value from state', () => {
  const mockState = { counter: { value: 10 } };
  expect(selectCount(mockState)).toEqual(10);
});

Integration Testing React Components with Redux

The Redux team recommends testing React components integrated with a real Redux store rather than mocking the store or the actions. This ensures that your components and state management work together as they would in production.

To achieve this, create a custom render helper that wraps the component under test in a Redux <Provider> configured with a fresh store instance for each test run.

Creating the Custom Render Helper

import React from 'react';
import { render } from '@testing-library/react';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import counterReducer from './counterSlice';

export function renderWithProviders(
  ui,
  {
    preloadedState = {},
    store = configureStore({ reducer: { counter: counterReducer }, preloadedState }),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>;
  }
  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

Writing the Component Test

With the helper function defined, you can render your component, interact with the UI using React Testing Library, and assert that the Redux state updates correctly.

import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import { renderWithProviders } from './test-utils';
import CounterComponent from './CounterComponent';

test('should increment the displayed count when the button is clicked', () => {
  renderWithProviders(<CounterComponent />, {
    preloadedState: { counter: { value: 10 } }
  });

  const button = screen.getByText(/increment/i);
  const countDisplay = screen.getByTestId('count-value');

  expect(countDisplay.textContent).toBe('10');
  
  fireEvent.click(button);
  
  expect(countDisplay.textContent).toBe('11');
});