Creating Reusable Mocks

Now that you know how to use libraries and prebuilt mocks, it is time for us to develop our own mocks.

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:

  1. Request the list of tasks on the render.
  2. Make them available to the component as soon as they arrive.
  3. 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, under result.current, you will find the return value from useTasks.

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:

  1. Setting up mocks for API functions (getTasks, in this case, in api/index.js)
  2. 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 mock awaited values. mockResolvedValue(value) is identical to mockReturnValue(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 implementation
  • mockFn.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.