Mocking Arbitrary Imports: Continued

Continue developing the persistence feature, and explore mocks in greater depth.

Where we left off

In the last lesson, we introduced a new persistence feature to our app, and wrote an integration test for it. The test boils down to:

  1. Create a task.
  2. Refresh the page.
  3. Make sure the task is there.
  4. Mark task as completed.
  5. Refresh the page.
  6. Make sure it stays completed.

Next, we will write unit tests that validate each step of that scenario.

API functions

Since we have set up json-server, our trusty backend, we will need to interface with it. I suggest that we first test and create helper functions to make API requests. We will put them in /api/index.js. That means their tests will be under /api/index.test.js. Go ahead and create this file, but leave it empty for now.

Let us take a step back, and think about what we want our API functions to do. To begin with, we will need three functions:

  • createTask: To create a task.
  • updateTask: To update task’s info.
  • getTasks: To get all tasks.

Go ahead and create describe blocks for each of these, and add the (nonexistent) import line:

import {createTask, getTasks, updateTask} from './index';

describe('#createTask', () => {});

describe('#updateTask', () => {});

describe('#getTasks', () => {});

Now comes the interesting part. All of these functions will need to use fetch to make HTTP requests. This means we would have to mock the fetch function. Luckily, there is no need to reinvent the wheel, and we can use the mock that someone else made for us (later we would learn how to make our own mocks; do not worry). The library is called jest-fetch-mock, and you can install it like this:

$ npm i -D jest-fetch-mock

After you have installed it, it needs to be enabled. Open the file setupTests.js under src (this file is supplied by CRA), and add this line to it:

fetchMock.enableMocks();

That is it! Now all the calls to fetch (while testing) will be mocked, and a few useful functions will be available.

#getTasks

Learn the power of jest-fetch-mock by writing the test for getTasks. For this function, we need to:

  1. Verify that it GETs from the correct URL.
  2. Verify that it returns correct data.

To verify the return data, we will have to make fetch return something. We could use .mockReturnValueOnce, but fetch is funny. To get the results, you would have to call .json and await on it. To make life easier, developers of jest-fetch-mock included a very useful function mockResponseOnce. This function will take the desired return value and make it so fetch almost does an HTTP request and returns data. This is how we begin the test:

describe('#getTasks', () => {
    it('must get tasks', async () => {
        fetch.mockResponseOnce(JSON.stringify([{id: 1, label: '1', completed: true}]));
    });
});

Now, we will try to get the tasks:

const result = await getTasks();

And verify it:

expect(result.length).toBe(1);
expect(result[0]).toEqual({id: 1, label: '1', completed: true});

Are we forgetting something? Yes, we still need to verify that fetch was called with the correct arguments. However, it is hard to do since the host and port may change across machines. We should not write:

expect(fetch).toHaveBeenCalledWith('http://localhost:3001/tasks');

In this case, it would be enough to just make sure it was called with a URL that has tasks in it. You can use this instruction to do that:

expect(fetch).toHaveBeenCalledWith(expect.stringMatching(/tasks/);

This line verifies that fetch was called with a string that contains tasks, and this should be good enough.

expect.stringMatching(regex) looks different from the regular expect(something).toBe(something else) pattern. This instruction is designed to be used in place of arguments to verify mocks. Another useful function is expect.anything(). It is used when you need to know that there was an argument, but do not care which one.

Obviously, this test will fail as the getTasks function is not written yet. But now, with a solid idea of how that function behaves, it would be a breeze to write it (in api/index.js):

const ROOT = 'http://localhost:3001/tasks';

export const getTasks = async () => {
  const result = await fetch(ROOT);
  return result.json();
}

You probably noticed that I do not care for network (or any other) errors in this code. As an exercise, come up with an error-handling behaviour, write a test for it, and then the implementation.

These five lines will make that test pass and would even work with our backend. Now, let’s test the updateTask function.

Get hands-on with 1400+ tech skills courses.