How to Test Redux Selectors in React

Testing Redux selectors is a crucial step in ensuring your React application’s state management remains predictable and efficient. This article provides a straightforward guide on how to write unit tests for both basic and memoized Redux selectors using Jest. You will learn how to mock the Redux state, assert selector outputs, and isolate selector logic to keep your test suite fast and reliable.

Understanding Redux Selectors

Selectors are pure functions used to extract, format, or compute pieces of state from the Redux store. Because they are pure functions, testing them is highly straightforward: you pass a mock state object as an argument and assert that the selector returns the expected output.

Testing Basic Selectors

A basic selector retrieves a value directly from the state without any complex transformation.

Suppose you have the following basic selector:

// selectors.js
export const selectUserEmail = (state) => state.user.email;

To test this selector, you only need to construct a mock state object that mirrors the structure of your Redux store and pass it to the function.

// selectors.test.js
import { selectUserEmail } from './selectors';

describe('selectUserEmail', () => {
  it('should return the user email from the state', () => {
    const mockState = {
      user: {
        email: 'user@example.com',
        name: 'John Doe'
      }
    };

    const result = selectUserEmail(mockState);
    expect(result).toEqual('user@example.com');
  });

  it('should handle undefined state gracefully if safety checks are implemented', () => {
    const mockState = { user: {} };
    const result = selectUserEmail(mockState);
    expect(result).toBeUndefined();
  });
});

Testing Memoized Selectors (Reselect)

Memoized selectors, often created using the reselect library’s createSelector function, combine multiple input selectors to compute derived state. Testing these follows the same pattern as basic selectors, but you should also verify that the underlying logic/computation works correctly.

Suppose you have the following memoized selector:

// selectors.js
import { createSelector } from 'reselect';

const selectItems = (state) => state.cart.items;
export const selectDiscount = (state) => state.cart.discount;

export const selectCartTotal = createSelector(
  [selectItems, selectDiscount],
  (items, discount) => {
    const total = items.reduce((sum, item) => sum + item.price, 0);
    return total - discount;
  }
);

Method 1: Testing with Mock State

You can test the selector by passing a mock state object, just like a basic selector. This ensures the entire selector chain works together.

// selectors.test.js
import { selectCartTotal } from './selectors';

describe('selectCartTotal', () => {
  it('should calculate the total price of items minus the discount', () => {
    const mockState = {
      cart: {
        items: [
          { id: 1, price: 10 },
          { id: 2, price: 20 }
        ],
        discount: 5
      }
    };

    const result = selectCartTotal(mockState);
    expect(result).toEqual(25); // (10 + 20) - 5
  });
});

Method 2: Testing the Composed Function Directly (.resultFunc)

If your state structure is deeply nested or complex, you might want to test the computation logic of a memoized selector in isolation. Libraries like Reselect expose a .resultFunc property on the created selector. This allows you to pass the inputs directly to the projection function, bypassing the input selectors and the mock state entirely.

// selectors.test.js
import { selectCartTotal } from './selectors';

describe('selectCartTotal logic isolation', () => {
  it('should correctly calculate the total given mocked input values', () => {
    const mockItems = [{ price: 50 }, { price: 30 }];
    const mockDiscount = 10;

    // Use .resultFunc to pass dependencies directly as arguments
    const result = selectCartTotal.resultFunc(mockItems, mockDiscount);
    
    expect(result).toEqual(70); // (50 + 30) - 10
  });
});

Using .resultFunc is highly recommended for complex selectors, as it decouples your selector logic tests from the shape of your overall Redux state tree.