In this week’s post I will attempt to answer a question I got for a previous blog post I published,...
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.
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, 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.