DEV Community

Fredrik Bergqvist
Fredrik Bergqvist

Posted on • Updated on • Originally published at bergqvist.it

Mocking redux useSelector-hook

Update
There is an official way of using RTL with redux as some people pointed out in the comments but I never got it to work.
It may be me being incompetent or something in my project that causes issues so my solution to only mock 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 Enzymes mount.

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

The component that I wanted to test as 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 (...)
}
Enter fullscreen mode Exit fullscreen mode

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", () => ({
  ...jest.requireActual("react-redux"),
  useSelector: jest.fn()
    .mockReturnValueOnce(mockConfigState)
    .mockReturnValueOnce(mockSearchState)
}));

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

Enter fullscreen mode Exit fullscreen mode

Which worked fine until I realised 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", () => ({
  ...jest.requireActual("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: {
          ...mockShoppingState.config,
          query: ""
        }
      };
      useSelector.mockImplementation(callback => {
        return callback(localState);
      });
    const { queryByTestId } = render(<MySearchComponent />);
    expect(queryByTestId("query_testId")).toBeNull();
  });
});

Enter fullscreen mode Exit fullscreen mode

So in the code above I mock useSelector from the react-redux npm package and replaces 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.

Top comments (25)

Collapse
 
murilovarela profile image
Murilo Varela

In the RTL documentation, you can find an explanation of how you can wrap your component with the store provider. (testing-library.com/docs/example-r...)

I like to test whether my actions are called or not, and also pass a custom initialState. The aproach I use is the following above:

export function renderWithRedux(ui, { initialState = initialStateOriginal } = {}) {
  const actions = [];
  const observerMiddleware = () => next => action => {
    actions.push(action);
    return next(action);
  };
  const store = createStore(reducer, initialState, applyMiddleware(observerMiddleware));
  const utils = {
    dispatch(action) {
      return store.dispatch(action);
    },
    getDispatchedActions() {
      return actions;
    },
    getState() {
      return store.getState();
    },
  };
  return {
    ...render(<Provider store={store}>{ui}</Provider>),
    ...utils,
  };
}
Enter fullscreen mode Exit fullscreen mode

So you can get the dispatch, getDispatchedActions, and getState from the render result.

const { getByText, getDispatchedActions, getAllByTestId, dispatch } = renderWithRedux(
      <MyComponent />,
      { initialState: {......} }
    );
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fredrikbergqvist profile image
Fredrik Bergqvist

Very nice, thanks for the suggestion!

Collapse
 
gugol2 profile image
Watchmaker

When I do this, my calls to the components inside MyComponent get called just twice as many times as before using useSelector...

Somehow useSelector is provoking more re-renders.

Collapse
 
gugol2 profile image
Watchmaker • Edited

I found why...
Selectors have to return only one single field of the state that can be ONLY calculated used itself and no other slices of the state.
Otherwise you have to use Reselect or another library to create memoized selectors that compute and/or return multiple fields of the state.

My experience is that if your operations on the state are a bit complex maybe useSelector adds a bit too much complexity for what if offers and in that case it is maybe better stay with the connect HOC and its mapStateToProp function to operate on the state.
But if your operations on the state are simple useSelector may clear up your component a bit.

Collapse
 
ajinkyax profile image
Ajinkya Borade

you forgot to mention about Render.

import { render as rtlRender } from '@testing-library/react'
...

function render (
  ui,
  {
    initialState,
    store = configureStore({
      reducer: {
        env: envReducer
      }
    }),
    ...renderOptions
  } = {}
) {
  const Wrapper = ({ children }) => (
    <Provider store={store}>{children}</Provider>
  )
  return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
gokulmathew profile image
gokulmathew

I am getting this error.
TypeError: _reactReedux.useSelector.mockImplementation is not a function.
Kindly someone help

Collapse
 
mikeour profile image
Mike Roeslein

Could try casting it as a mock: (useSelector as jest.Mock).mockImplementation(...)

Collapse
 
gokulmathew profile image
gokulmathew

Tried that also dude, but didnt work. Added how to test useDispatch of react hooks? using react testing library.

Thread Thread
 
fredrikbergqvist profile image
Fredrik Bergqvist

It does not seem like redux is being mocked (looking for mockImplementation in react redux)

In the code below I've listed all moving parts of the test

  1. You need to import the react-redux library
  2. You need to mock the function
  3. You need to add the mockImplementation to provide a response value

Having all three pieces should make it work.

import { useSelector } from "react-redux";

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

describe("MySearchComponent", () => {
  beforeEach(() => {
    useSelector.mockImplementation(callback => {
      return callback(mockAppState);
    });
  });
  test(() => {
    // Your test here
  });
});
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
anshumanburman profile image
anshumanburman

what is "mockAppState"? can you please explain me more?

Thread Thread
 
fredrikbergqvist profile image
Fredrik Bergqvist

mockAppState is the mocked redux state that your component needs to be able to run.
It should include data for all redux nodes that the component is using.

Take a component like this:

//the redux state looks like this;
// {
//   name: "Fredrik",
//   title: "Developer"
// }
import { useSelector } from "react-redux";
const MyComponent = () => {
  const reduxState = useSelector((state) => state)
  return (<h1>Hello {reduxState.name}</h1>)
}
Enter fullscreen mode Exit fullscreen mode

A mocked app state for the above component would look like this:

{
  name: "Mocked name",
 }
Enter fullscreen mode Exit fullscreen mode

So that you can run tests against a state that you have full control over.

Collapse
 
wolverineks profile image
Kevin Sullivan

Would it be the same/simpler to render the component in a ?

Collapse
 
fredrikbergqvist profile image
Fredrik Bergqvist

I think you might have forgot a word there :)

In Enzyme I would wrap the component in a <provider> but RTL did not allow that and I find it cleaner and more explicit to mock useSelector.

Collapse
 
markerikson profile image
Mark Erikson • Edited

What do you mean by "did not allow that"?

RTL's docs specifically show how to use it with React-Redux:

function renderWithRedux(
  ui,
  { initialState, store = createStore(reducer, initialState) } = {}
) {
  return {
    ...render(<Provider store={store}>{ui}</Provider>),
    // adding `store` to the returned utilities to allow us
    // to reference it in our tests (just try to avoid using
    // this to test implementation details).
    store,
  }
}

// then, in a test:
const { getByTestId, getByText } = renderWithRedux(<Counter />)
Thread Thread
 
fredrikbergqvist profile image
Fredrik Bergqvist

Well to be honest I did add it to the render method, noticed that I got an error and took the other way out :)

Thread Thread
 
timrohrer profile image
tim.rohrer

Another dev (way more experienced than me) also mocks useSelector.

I would like to see the "official" way work, but so far I'm still stuck.

In my component, a useSelector that includes the callback definition works. However, any useSelector that requires an imported callback seems to remain undefined.

This works:

  let demoTrip = useSelector(
    (state: RootState) => state.trips.allTrips[demoTripId]
  );

This returns undefined:

import { selectChosenMappingService } from '../system/systemSlice';  
...
const mappingServiceName = useSelector(selectChosenMappingService);

And from my slice:

export const selectChosenMappingService = (
  state: SystemState
): MappingService => state.chosenMappingService;

I remain confused.

Thread Thread
 
timrohrer profile image
tim.rohrer

I am happy to report that my issue was a mistake in my definitions of my selector functions in my slice. Turns out I needed to type as RootState and then include the reducer in the return (i.e., state.system.chosenMappingService).

Collapse
 
lxuanchengdx profile image
lxuanchengdx

Hey Fredrik, thank a lot! it really helps me right now.

Collapse
 
fredrikbergqvist profile image
Fredrik Bergqvist

Thank you! Glad it could be of some help :)

Collapse
 
joaodos3v profile image
João Vitor Veronese Vieira

It saved my time! Thank you :)

Collapse
 
thisthat90 profile image
thisthat

Instead of requireActual, can we use Jest.spyOn? For me, the below code seems to work fine.

jest
.spyOn(Redux, "useSelector")
.mockImplementation((callback) => callback(mockAppState));

Some comments may only be visible to logged-in visitors. Sign in to view all comments.