In this week’s post I will attempt to answer a question I got for a previous blog post I published, regarding mocking a custom React hook which fetches data from the server.
I got a question on one of the posts I made some time ago called “TDD with MSW for a Custom Fetch React Hook”, and I thought it would be nice to try and answer it in a post and also describe my thought process for these sorts of test dilemmas.
You can find the question here, but let me TL;DR for you -
Say you have a custom hook which fetches data from the server, in a stale-while-revalidate strategy, and you wish to test the component which uses it to display the data accordingly, but you don’t want to fetch the data from the server. How would you go about it?
(was it TL;DR…? perhaps not ;)
The code given as the sandbox for the question can be found here.
So let’s start by understanding what we have on our table -
We’re dealing with unit tests here, and unit tests are not integration tests. Perhaps this sentence needs some repetition:
unit tests are not integration tests.
This means that we have no intention of making any requests, or mocking any requests from our test.
We’re not testing the hook here, oh no we don’t. What we’re interested in is testing the actual code which uses the data the hook fetches. We don’t care how the hook does that.
In many cases, not understanding this separation of concerns (SoC) and trying to test everything, causes our tests to be complex, scattered all around, too long and most disturbing of all - slow.
Now that we are on the same page, let’s continue -
We have the custom hook’s code. It uses the useSWR hook which knows how to manage a stale-while-revalidate fetching strategy. Here it is:
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((res) => res.json());
function useGames() {
const {data, error} = useSWR(() => 'https://5fbc07c3c09c200016d41656.mockapi.io/api/v1/games', fetcher);
if (error) {
// TODO: handle if API fails
}
return {Games: data, GamesError: error};
}
export {useGames};
And here is the code for the component (“page” if you wish) that uses this hook:
import React from 'react';
import {useGames} from '../hooks/Games';
export default function Home() {
const {Games, GamesError} = useGames();
if (GamesError) {
return <>Error loading Games</>;
}
if (!Games) {
return <>loading...</>;
}
return (
<div>
{Games.map((game, index) => {
return (
<React.Fragment key={game?.id}>
<h1>{game?.name}</h1>
<h3>{game?.genre}</h3>
</React.Fragment>
);
})}
</div>
);
}
P.S. I modified it a bit, just to demonstrate better.
What this does is basically fetching game titles and then displaying them, each by its name and genre.
Ok, now that we have this, let’s write a simple test which checks that the Home component is rendered in a “loading…” state if there are no games:
import {render, screen} from '@testing-library/react';
import Home from './Home';
describe('Home page', () => {
it('should render in a loading state', () => {
render(<Home />);
const loadingElement = screen.queryByText('loading...');
expect(loadingElement).toBeInTheDocument();
});
});
The test passes. Great.
We would like to check now, that if there are games, our component displays what it should. For that we will need to mock our hook.
The hook, like any other hook, is nothing special really. It is a mere function that may receive input and returns values or functions we can use or invoke.
So first of all let’s see how we mock the hook:
const mock = {Games: null, GamesError: null};
jest.mock('../hooks/Games', () => ({
useGames: () => {
return mock;
},
}));
Remember that jest mocks are hoisted to the top of the test, but written as it is above it won’t cause any issue with non-initialized variables, since the mock variable only gets used when the useGames method is invoked.
This allows use to write the following test case:
it('should display the games according to the hooks data', () => {
mock.Games = [
{
id: '1',
name: 'name 1',
genre: 'Alda Kling',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jm_denis/128.jpg',
editor_choice: false,
platform: 'platform 1',
},
{
id: '2',
name: 'name 2',
genre: 'Haylie Dicki',
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/netonet_il/128.jpg',
editor_choice: false,
platform: 'platform 2',
},
];
render(<Home />);
const loadingElement = screen.queryByText('loading...');
expect(loadingElement).not.toBeInTheDocument();
const game1Element = screen.queryByText('name 1');
expect(game1Element).toBeInTheDocument();
const game2Element = screen.queryByText('name 2');
expect(game2Element).toBeInTheDocument();
});
In the code above we populate the mock with 2 games, and then we assert that the “loading…” element is not on the document (since we have data) and that we have 2 games displayed: “name 1” and “name 2”.
That’s it pretty much.
We did not need to mock the requests, or fake anything which is network related (we might wanna do that when testing the actual hook, using MSW as described in my previous article), but I think that this approach tests what needs to be tested, quickly and simply.
Notice that I didn’t care about the strategy the hook is fetching the data with - whether it is SWR or not.
It is important to always ask yourself “what do I want to test here?” is fetching the content the page’s concern or maybe it’s the hook’s concern? Am I testing the hook’s functionality here or just how my component reacts to its different states?
As always if you have any questions or think of better ways to approach what you’ve just read, be sure to leave them in the comments below so that we can all learn.
Hey! If you liked what you've just read check out @mattibarzeev on Twitter 🍻
Photo by Joe Dudeck on Unsplash
Top comments (5)
Thank you!! I was searching so long to find someone who finally gets what I was trying to do to make my unit tests to be reliable.
My pleasure! In tests we trust ;)
This is really a great help. I understand difference between unit tests and integration testing better now. Thank you for the reply post. 😊
So glad to hear that 😃
Hi,This is my custom hook
const useHeaderMenu = () => {
const authValue = localStorageService.readToken();
}
I need to write test case for this.Actually in my component inside useEffect i called getHeaderMenu .Its return some menuList and logo.I need to write test case for logo and menulist should be rendered.How can i do this.Please advice me.Thanks