How to Test Redux Store in React

Testing the Redux store in a React application ensures that state transitions are predictable and that components interact correctly with the global state. This article provides a straightforward, step-by-step guide on how to unit test Redux reducers and actions, as well as how to perform integration tests on Redux-connected components using Jest and React Testing Library. By the end of this guide, you will know how to set up a mock store environment and write reliable tests for your Redux state management.

Unit 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, unit testing them is simple and does not require mocking the Redux store.

To test a reducer, pass an initial state and an action to the reducer function, then assert that the returned state matches your expectations.

// counterSlice.js
export const counterReducer = (state = { value: 0 }, action) => {
  switch (action.type) {
    case 'increment':
      return { value: state.value + 1 };
    default:
      return state;
  }
};

// counterReducer.test.js
import { counterReducer } from './counterSlice';

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

  it('should handle increment', () => {
    expect(counterReducer({ value: 1 }, { type: 'increment' })).toEqual({ value: 2 });
  });
});

Unit Testing Action Creators

Action creators are functions that return action objects. You can test them by calling the function and asserting that it returns the correct action type and payload.

// actions.js
export const increment = () => ({ type: 'increment' });

// actions.test.js
import { increment } from './actions';

describe('action creators', () => {
  it('should create an action to increment the counter', () => {
    const expectedAction = { type: 'increment' };
    expect(increment()).toEqual(expectedAction);
  });
});

Integration Testing: Redux with React Components

The official Redux team recommends testing Redux-connected components with a real Redux store rather than mocking the store or the actions. This ensures that the component, selectors, reducers, and actions all work together seamlessly.

1. Create a Helper Function for Rendering

To avoid repeating store configuration in every test file, create a helper function that wraps the React Testing Library’s render method with a <Provider> component initialized with a fresh store instance.

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

function renderWithProviders(
  ui,
  {
    preloadedState = {},
    // Create a new store instance for every test run
    store = configureStore({ reducer: { counter: counterReducer }, preloadedState }),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>;
  }
  return { store, ...rtlRender(ui, { wrapper: Wrapper, ...renderOptions }) };
}

export * from '@testing-library/react';
export { renderWithProviders };

2. Write Component Integration Tests

Using the renderWithProviders helper, you can now write tests that interact with your components and verify that the Redux store state updates correctly.

// CounterComponent.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

export function CounterComponent() {
  const count = useSelector((state) => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <span data-testid="count-value">{count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
    </div>
  );
}

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

describe('CounterComponent integration', () => {
  it('should render with initial state from Redux', () => {
    renderWithProviders(<CounterComponent />, {
      preloadedState: { counter: { value: 10 } },
    });

    expect(screen.getByTestId('count-value').textContent).toBe('10');
  });

  it('should increment the state value when the button is clicked', () => {
    renderWithProviders(<CounterComponent />, {
      preloadedState: { counter: { value: 0 } },
    });

    const button = screen.getByText('Increment');
    fireEvent.click(button);

    expect(screen.getByTestId('count-value').textContent).toBe('1');
  });
});