How to Test Redux Toolkit in React

Testing Redux Toolkit (RTK) in React ensures that your application’s state management logic and UI components work seamlessly together. This guide provides a practical overview of how to unit test Redux slices (reducers and actions) and how to write integration tests for React components connected to the Redux store. You will learn how to isolate state logic for unit tests and how to configure a reusable test wrapper using React Testing Library for full component integration tests.

Unit Testing Redux Slices

Because Redux Toolkit reducers are pure functions, they are straightforward to unit test. You do not need to configure a full Redux store to test slice reducers; you simply pass an initial state and an action to the reducer and assert the returned state.

Suppose you have a simple counter slice:

// counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const initialState = { value: 0 };

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

To unit test this reducer, import the reducer and action creators into your test file:

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

describe('counter reducer', () => {
  const initialState = { value: 0 };

  test('should handle initial state', () => {
    expect(counterReducer(undefined, { type: 'unknown' })).toEqual({ value: 0 });
  });

  test('should handle increment', () => {
    const actual = counterReducer(initialState, increment());
    expect(actual.value).toEqual(1);
  });

  test('should handle decrement', () => {
    const actual = counterReducer({ value: 5 }, decrement());
    expect(actual.value).toEqual(4);
  });
});

Integration Testing React Components

Testing components in isolation by mocking the Redux store can lead to brittle tests. The recommended approach is to test the component’s integration with a real Redux store configured specifically for each test run. This prevents state from leaking between tests.

1. Creating a Custom Render Helper

To avoid duplicating Redux store setup in every test file, create a custom render function. This helper wraps the React Testing Library render function and automatically injects a provider with a fresh store instance.

// test-utils.jsx
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. Testing the Component

With the custom render helper ready, you can now write integration tests for your connected components. This allows you to interact with the UI using user events and verify that the Redux state updates appropriately.

Suppose you have a Counter component:

// Counter.jsx
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

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

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

You can test this component by rendering it with your custom renderWithProviders utility:

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

describe('Counter Component', () => {
  test('should display initial state value', () => {
    renderWithProviders(<Counter />, {
      preloadedState: { counter: { value: 10 } },
    });

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

  test('should increment state when button is clicked', () => {
    renderWithProviders(<Counter />);
    
    const incrementButton = screen.getByText('Increment');
    const countValue = screen.getByTestId('count-value');

    expect(countValue.textContent).toBe('0');
    fireEvent.click(incrementButton);
    expect(countValue.textContent).toBe('1');
  });
});