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 (...)
}
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(...)
});
});
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();
});
});
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)
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:
So you can get the dispatch, getDispatchedActions, and getState from the render result.
Very nice, thanks for the suggestion!
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.
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.
you forgot to mention about Render.
I am getting this error.
TypeError: _reactReedux.useSelector.mockImplementation is not a function.
Kindly someone help
Could try casting it as a mock: (useSelector as jest.Mock).mockImplementation(...)
Tried that also dude, but didnt work. Added how to test useDispatch of react hooks? using react testing library.
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
Having all three pieces should make it work.
what is "mockAppState"? can you please explain me more?
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:
A mocked app state for the above component would look like this:
So that you can run tests against a state that you have full control over.
Would it be the same/simpler to render the component in a ?
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.What do you mean by "did not allow that"?
RTL's docs specifically show how to use it with React-Redux:
Well to be honest I did add it to the render method, noticed that I got an error and took the other way out :)
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, anyuseSelector
that requires an imported callback seems to remain undefined.This works:
This returns undefined:
And from my slice:
I remain confused.
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
).Hey Fredrik, thank a lot! it really helps me right now.
Thank you! Glad it could be of some help :)
It saved my time! Thank you :)
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.