The Power of Mocking in Unit Tests
June 16, 2023 - 7 min read
intermediate
react
typescript
Testing is often an afterthought when writing code. It can be confusing, slow down development,
and if implemented poorly provides questionable utility. The following are some tips for
preemptively writing and structuring your code so that it is easy to unit test.
Mocking Test Boundaries
When using React, composition is key. This often means that if you want to test a component,
you will by default be rendering all of the children of that component. This can be problematic
in a large application where you can end up rendering dozens or even hundreds of components.
Here is an example of a component that we want to write a unit test for. It is a Search component
that contains a search input, a button that triggers the search, and a component that is the
list of all search results.
import { SearchInput } from 'components/SearchInput'; import { SearchResults } from 'components/SearchResults'; interface Props { search: () => void; } function Search({ search }: Props) { return ( <Flex> <SearchInput /> <Button data-testid="search-button" onClick={() => search()}> Search </Button> <SearchResults /> </Flex> ); }
Since
SearchInput
and SearchResults
are separate components, ideally the unit
tests for those should be written separately. This leaves us with testing
the button to make sure that when it is clicked, the search
function is called.
We don't want to render the other components, otherwise they may potentially fail
the unit test and we wouldn't know if the Search
component caused the failure,
or if it was caused by another component deeper down the component tree.Using module mocking we can prevent the search input and search results from rendering
so that we can write a unit test that only deals with the search button. The following
snippet uses Jest's
mock
function to render stubbed components with a test-id.jest.mock('components/SearchInput', () => ({ SearchInput: () => <div data-testid="search-input" />, })); jest.mock('components/SearchResults', () => ({ SearchResults: () => <div data-testid="search-results" />, }));
Now, when we render our
Search
component, we end up with something that looks like this:<Flex> <div data-testid="search-input" /> <Button data-testid="search-button" onClick={() => search()}> Search </Button> <div data-testid="search-results" /> </Flex>
At this point it is very easy to write a unit test that checks the
search
function is called
when the button is clicked.// provide a mocked function for the search prop const props = { search: jest.fn(), }; const { getByTestId } = render(<Search {...props} />); // find the button by test id const button = getByTestId('search-button'); // fire a click event for the button fireEvent.click(button); // check that the search prop has been called 1 time expect(props.search).toHaveBeenCalledTimes(1);
Mocking Environment Variables
If you structure your fetching code in React hooks, you will probably end up
with hooks that look like this:
async function fetchUsers() { const response = await fetch(`${process.env.API_BASE_URL}/users`); return await response.json(); } function useFetchUsers() { // using react-query return useQuery(['users'], () => fetchUsers()); }
It can be annoying to mock environment variables like
process.env.API_BASE_URL
.
A better way to structure this code is to have a separate file for environment
variables that you can import as a module.Environment variables module:
// config/constants.ts export const API_BASE_URL = process.env.API_BASE_URL || '';
Then you can update the
fetchUsers
function to look like this:import { API_BASE_URL } from 'config/constants'; async function fetchUsers() { const response = await fetch(`${API_BASE_URL}/users`); return await response.json(); }
With this setup, it is very easy to use
jest.mock
to change the value of API_BASE_URL
to whatever you'd like, and skip needing to fiddle with mocking process.env
values.jest.mock('config/constants', () => ({ API_BASE_URL: 'whatever-value-youd-like', }));
Use Props Instead of Complex Code Directly in UI Components
When creating a UI in React, it is often tempting to grab any hooks or values that you need
and include them alongside your UI components. This becomes very annoying when writing unit
tests because you'll find that you need to mock a bunch of different modules and functionality
before you can render your UI. It is much easier to separate your components so that your UI
component accepts all of the props it needs to render the UI as a pure function.
🚫 Including everything in the UI component
function UsersList() { // translation hook for i18n const { t } = useTranslation(); // hook to fetch users const { users, isLoading, isError } = useFetchUsers(); return ( <Flex direction="column"> <Flex> <Flex>{t('firstName', 'First Name')}</Flex> <Flex>{t('lastName', 'Last Name')}</Flex> </Flex> {users.map((user) => ( <Flex key={user.id}> <Flex>{user.firstName}</Flex> <Flex>{user.lastName}</Flex> </Flex> ))} </Flex> ); }
✅ Passing props to a pure UI component
interface Props { t: TranslationFunction; users: User[]; isLoading: boolean; isError: boolean; } function UsersList({ t, users, isLoading, isError }: Props) { return ( <Flex direction="column"> <Flex> <Flex>{t('firstName', 'First Name')}</Flex> <Flex>{t('lastName', 'Last Name')}</Flex> </Flex> {users.map((user) => ( <Flex key={user.id}> <Flex>{user.firstName}</Flex> <Flex>{user.lastName}</Flex> </Flex> ))} </Flex> ); }
With the UI component set up like this, it is very easy to write different testing scenarios
by tweaking the props that are passed.
const baseProps = { t: mockTranslationFn(), users: [], isLoading: false, isError: false, }; test('when loading', () => { const props = { ...baseProps, isLoading: true, }; // render the UI component... }); test('when error', () => { const props = { ...baseProps, isError: true, }; // render the UI component... }); test('when users', () => { const props = { ...baseProps, users: [{ id: 1, firstName: 'test', lastName: 'user' }], }; // render the UI component... });
This setup is part of a larger strategy of structuring your React code as separate
Container and UI components. I will discuss this topic in a future blog post.
Mock Implementations
Sometimes it is not enough to mock a module. Instead, you'd like to mock a module and have
multiple implementations of that module to test different scenarios. Let's say we have a hook
that fetches users and returns an array of users and some loading and error state.
import { useFetchUsers } from 'hooks/useFetchUsers'; function ContainerComponent() { // we need to mock this hook const { users, isLoadingUsers, isErrorUsers } = useFetchUsers(); }
Lets start by using
jest.mock
to mock the module.jest.mock('hooks/useFetchUsers');
By default, jest will mock the module, but it won't provide us with useful return values
to write unit tests against. In order to do that, we need to set up different mock implementations
of the hook.
jest.mock('hooks/useFetchUsers'); import { useFetchUsers } from 'hooks/useFetchUsers'; // necessary for Typescript to understand this is a mock const mockedUseFetchUsers = jest.mocked(useFetchUsers); test('when returns users', () => { mockedUseFetchUsers.mockReturnValue({ // return users users: [{ id: 1, firstName: 'test', lastName: 'user' }], isFetchingUsers: false, isErrorUsers: false, }); // write a test against this scenario }); test('when error', () => { mockedUseFetchUsers.mockReturnValue({ users: [], isFetchingUsers: false, // error is true isErrorUsers: true, }); // write a test against this scenario });
These are just some of the many things you can do with mocking when writing unit tests. Hopefully
these tips help you to write better unit tests!