How to mock the Redux useSelector hook
Fredrik BergqvistThere 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.