Skip to main content

Top level navigation menu

A drawn image of Fredrik Bergqvist in a blue shirt

How to mock the Redux useSelector hook

Fredrik Bergqvist

There is an official way of using RTL with redux as some people pointed out, but for small quick tests mocking useSelector may still be of use. 🙄

Recently I finally made the switch from Enzyme to React testing library (RTL) which also means that instead of rendering components using shallow like Enzyme proposes, with "React testing library" the whole component and its child components is rendered, much like Enzyme's mount.

The switch to RTL coupled with using hooks instead of HOCs when using Redux got me writing a lot of new component tests. However, I did run into some problems when I tried to use the useSelector hook from Redux multiple times expecting different responses.

The component that I wanted to test is a search component that made calls similar to this:

const MySearchComponent = () => {
  const { query, rows } = useSelector((state) => state.config);

  const { items, hasMore } = useSelector((state) => state.search);

  return (<>...</>);
}

useSelector takes a callback function that takes the state as an argument and returns a slice of the state.

So my first approach when trying to test the component was to send two different responses.

jest.mock("react-redux", () => ({
  useSelector: jest.fn()
    .mockReturnValueOnce(mockConfigState)
    .mockReturnValueOnce(mockSearchState)
}));

describe("MySearchComponent", () => {
  afterEach(() => {
    useSelector.mockClear();
  });
  it("should render", () => {
    const { getByTestId } = render(<MySearchComponent />);
    expect(...);
  });
});

Which worked fine until I realized that a child component also calls useSelector and therefore crashed. 😱

I knew I needed something that would support all possible selectors that I needed but still could be modified on a test by test basis. I had a mock state ready, but not the method to alter and inject it. Until I ran across jest.fn().mockImplementation

The solution to my problems

useSelector takes a callback as its argument and all I had to do was to call that callback with a compatible state that would satisfy all my components' needs, and they would do the rest as implemented.

jest.mock("react-redux", () => ({
  useSelector: jest.fn()
}));

describe("MySearchComponent", () => {
  beforeEach(() => {
    useSelector.mockImplementation(callback => {
      return callback(mockAppState);
    });
  });
  afterEach(() => {
    useSelector.mockClear();
  });
  it("should render a query", () => {
    const { getByTestId } = render(<MySearchComponent />);
    expect(getByTestId("query_testId").textContent)
      .toEqual(mockAppState.config.query);
  });
  it("should not render if query is empty", () => {
    const localMockState = {
      ...mockAppState,
      config: {
        ...mockAppState.config,
        query: ""
      }
    };
    useSelector.mockImplementation(callback => {
      return callback(localMockState);
    });
    const { queryByTestId } = render(<MySearchComponent />);
    expect(queryByTestId("query_testId")).toBeNull();
  });
});

So in the code above I mock useSelector from the react-redux npm package and replace it with a function that executes any given callback function with my mocked state as an argument. This is done before every test.

In the second test I create a second mocked state that I want to use for just that test, so I override useSelector to make sure it uses my updated state instead of the default mock state.

Parting words

I hope this helped someone to learn a little bit more about how to test their code and what can be achieved with jest and tools like RTL (which is great, try it!)

All typos my own, and please leave a comment if you have a question or something does not make sense.

Site information

This site is built with Eleventy and hosted on Vercel.

Icons are from Flaticon.