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.

Discussion (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 Author

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 on

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 Author

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 Author

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 Author

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 on

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 Author

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 Author

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));

Collapse
sergey_dubovik_1db68b2f23 profile image
Sergey Dubovik • Edited on

Frederik, thanks for the article - really helped me testing certain component in our app!
For those who are stumble upon it and need to test components with multiple useSelector's, do as article suggests, and add this:

//Mock your selectors
jest.mock('path/to/selectors', () => {
    const ActualHelpers = jest.requireActual('path/to/selectors');
    return {
        ...ActualHelpers,
        selectorNumberOne: jest.fn(),
        selectorNumberTwo: jest.fn(),
    };
});

//and then inside of your it()/test():
selectorNumberOne.mockReturnValue('value you need')
selectorNumberTwo.mockReturnValue('value for selector #2') // and so on
Enter fullscreen mode Exit fullscreen mode
Collapse
sklinov profile image
Sergei

Thanks, Fredrick for the article and Sergey for that comment.
I have a slightly different setup: I've wrapped RTL render with my redux-dynamic-modules provider, so I think there's no need to mock react-redux or useSelector, and all I need to actually mock is the return value of my selector function.

I'm trying to implement it the way shown in your comment, but I'm getting an error that my selector is undefined.

Collapse
sergey_dubovik_1db68b2f23 profile image
Sergey Dubovik

Paste your code here

Thread Thread
sklinov profile image
Sergei • Edited on
...
import { userListSelector } from "@/redux/user/user.selector";

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

jest.mock("@/redux/user/user.selector", () => {
  const actualUserSelectors = jest.requireActual("@/redux/user/user.selector");
  return {
    ...actualUserSelectors,
    userListSelector: jest.fn(),
  };
});

describe("Render Component", () => {
  beforeEach(() => {
    documentBody = render(
      <Component />,
      undefined,
      [reduxUser()] // this is dynamic module wrapper
    );
    (useSelector as jest.MockedFunction<typeof useSelector>).mockImplementation((selector: any) => {
      return selector();
    });

    (userListSelector as jest.MockedFunction<typeof userListSelector>).mockReturnValue({
      loading: false,
      users: createMockList<UserType>(3),
    });
  });

  afterEach(() => {
    cleanup();
  });

  test("Should render ProfileTable", () => {
    expect(documentBody.getByTestId(TestId.Component)).toBeInTheDocument();
  });
});

Enter fullscreen mode Exit fullscreen mode

And I get Cannot destructure property 'users' of 'react_redux_1.useSelector(...)' as it is undefined. error from component