Creating Reusable Mocks
Now that you know how to use libraries and prebuilt mocks, it is time for us to develop our own mocks.
We'll cover the following
Interfacing components with API: useTasks
hook
Though we have our API helpers written, this is not enough to make tasks persistent. In order to connect the React components and the underlying API functions, create a useTasks
hook, which would take the responsibility of polling and updating tasks.
We want it to:
- Request the list of tasks on the render.
- Make them available to the component as soon as they arrive.
- Expose functions to create and toggle tasks.
As always, we start with writing the tests for this hook. Create a folder hooks
and a file useTasks.test.js
in it. Firstly, we will test that useTasks
requests tasks:
import useTasks from './useTasks';
describe('#useTasks', () => {
it('must request tasks', () => {});
});
Now, think about what we should call the useTasks
hook. If you have worked with React for some time, you should know that it is only possible to call hooks from within components, and our test is definitely not a component. Should we develop an entire component to test this hook? The obvious answer is no. We will use @testing-library
to render the hook.
Specifically, we will use the @testing-library/react-hooks
section.
It is not included in CRA by default, so we have to install it:
$ npm i -D @testing-library/react-hooks
Once you have it installed, a couple of very useful functions manifest themselves in your project. Firstly, the renderHook
function. It is imported like this:
import {renderHook} from '@testing-library/react-hooks';
And now you can safely call your hook:
const {result} = renderHook(() => useTasks());
renderHook
returns an object. In this object, underresult.current
, you will find the return value fromuseTasks
.
Now, we want to check the return value. This begs a question: if the hook makes a request on the first call, what will the return value be until data arrives from the server? Let’s make it null
. Also, our hook will return a list like this:
// somewhere in some component
const [tasks, {toggleTask, createTask}] = useTasks();
Now, back to our test. Here is how we could validate that it returns null
initially:
it('must request tasks', async () => {
const { result } = renderHook(() => useTasks());
expect(result.current[0]).toBe(null);
});
After the data comes back, however, null
would change to the actual tasks from the server. How do we wait until useTasks
triggers a re-render? We will use another function called waitForNextUpdate
:
const {result, waitForNextUpdate} = renderHook(() => useTasks());
expect(result.current[0]).toBe(null);
await waitForNextUpdate();
expect(result.current[0].length).toBe(2); //expect to get 2 tasks
The waitForNextUpdate
function is returned by renderHook
and will wait until the useTasks
hook triggers a re-render due to state update. Note that in case useTasks
does not trigger an update, this line will fail the test.
We now have a “bare bones” test for the non-existent useTasks
component. However, it is not complete without API interaction. These things are missing from the test currently:
- Setting up mocks for API functions (
getTasks
, in this case, inapi/index.js
) - Asserting that we called these functions.
In the last few lessons, we reviewed how to use the jest-fetch-mock
library to mock the fetch
function. Now, we will see how to mock any import with any function. To do this, you will need to use the jest.mock
function:
import {renderHook} from '@testing-library/react-hooks';
import useTasks from './useTasks';
import {getTasks} from '../api';
jest.mock('../api');
This simple line will mock every import of '../api'
under the testing code. To specify what the mock must look like, we need to supply a generator function:
jest.mock('../api', () => ({
getTasks: jest.fn()
});
Now, Jest will call the generator function and use its return value as the mock. In this case, mocked api
will expose one named mock function getTasks
. Now, we can do this in the test code:
getTasks.mockResolvedValueOnce([
{label: 'Do this', id: 0, completed: false},
{label: 'Do that', id: 1, completed: true}
]);
mockResolvedValueOnce
is used to mockawait
ed values.mockResolvedValue(value)
is identical tomockReturnValue(Promise.resolve(value))
.
Here is the entire test code if you were lost:
// /src/hooks/useTasks.test.js
import { renderHook } from '@testing-library/react-hooks';
import useTasks from './useTasks';
import {getTasks} from '../api';
jest.mock('../api', () => ({getTasks: jest.fn()}));
describe('#useTasks', () => {
it('must request tasks', async () => {
getTasks.mockResolvedValueOnce([
{label: 'Do this', id: 0, completed: false},
{label: 'Do that', id: 1, completed: true}
]);
const { result, waitForNextUpdate } = renderHook(() => useTasks());
expect(result.current[0]).toBe(null);
await waitForNextUpdate();
expect(result.current[0].length).toBe(2);
expect(result.current[0][0].label).toBe('Do this');
expect(getTasks).toHaveBeenCalled();
});
});
Reusable mocks
Creating mocks with a generator function and a bunch of mockResolvedValueOnce
is simple enough. But as your codebase grows, it can become a nightmare. Thankfully, Jest provides us with a way to create reusable mocks.
Think of those reusable mocks as a file that replaces the original one when Jest runs its tests. For this to work, you have to create mocked modules in the __mocks__
folder of the module that you want to mock. For example, to create mocks for api/index.js
, create a file api/__mocks__/index.js
. Since this file will replace the original one when running tests, it must expose the same functions (getTasks
, createTask
, and updateTask
):
// /src/api/__mocks__/index.js
export const createTask = jest.fn(taskName =>
Promise.resolve({
id: 1,
label: taskName,
completed: false
}));
export const updateTask = jest.fn(task => Promise.resolve(task));
export const getTasks = jest.fn(() =>
Promise.resolve([
{id: 1, label: 'Do this', completed: false},
{id: 2, label: 'Do that', completed: true}
])
);
This file exposes the same functions but with different implementations. Firstly, note that they are all created using the jest.fn
constructor. This is done to include the assertion functions (expect().toHaveBeenCalled
, etc.). As an argument, we pass in our mock implementation, which is close enough to reality to enable testing. Thus, getTasks
returns two simple tasks, createTask
emulates server response by returning a new task object, and updateTask
does the same by returning the updated task. Now, back in useTasks.test.js
, we can simplify the test to:
// /src/hooks/useTasks.test.js
import { renderHook } from '@testing-library/react-hooks';
import useTasks from './useTasks';
import {getTasks} from '../api';
jest.mock('../api');
describe('#useTasks', () => {
it('must request tasks', async () => {
const { result, waitForNextUpdate } = renderHook(() => useTasks());
expect(result.current[0]).toBe(null);
await waitForNextUpdate();
expect(result.current[0].length).toBe(2);
expect(result.current[0][0].label).toBe('Do this');
expect(getTasks).toHaveBeenCalled();
});
});
Notice that we no longer need the generator function in jest.mock
call. We also no longer need to specify the return value of getTasks
. Jest took care of all of this by substituting the original api/index.js
with api/__mocks__/index.js
.
Note that this approach lets you mock any module and not just your own ones. For example, if you wanted to mock the
redux
library on the whole project, you would create a file__mocks__/redux.js
in the project root.
Quick recap
In this lesson, we took a deep dive into mocking and creating reusable mocks. We wrote some testing for the new useTasks
hook and agreed on its interface. This is done all before writing any implementation code. We used such functions such as:
@testing-library/react-hooks
library to test hooks.renderHook(() => useSomething())
to render a hook.waitForNextUpdate()
to wait until a rendered hook triggers a re-render.jest.mock(module name, generator function)
to mock a module with a specified structure.jest.mock(module name)
to mock a module automatically or with a written mock under__mocks__
folder.jest.fn(implementation)
to have a mock function with specified implementationmockFn.mockResolvedValue[Once](value)
to mock a resolved return value.
In the following lesson, we will complete the tests and implementation for the useTasks
hook. Here is the entire project so far for reference (branch step-19
on Github):
Get hands-on with 1400+ tech skills courses.