Getting Started with Jest Mocks


In this video we are going to make use of our now nicely tested asyncFetch function. However, to do this we must now mock (using Jest's mocking capabilities) the asyncFetch calls, as we don't really want to send out any HTTP requests during our test runs.

To begin with, I'm going to rename the src/connectivity/api.js file to src/connectivity/api.auth.js. This is a more real world approach to how I work, aiming to keep relevant API calls grouped together in smaller files.

With this in place, we can then begin to write a test that covers the login function.

// /__tests__/connectivity/api.auth.js

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

    });
  });
});

In our test file we will firstly need to ensure that our test uses an async function, as will be await'ing the outcome of our call to api.login.

As we are adding tests to cover existing code, rather than working in a true test-driven development manner, we can quickly recap the code we are testing:

// /src/connectivity/api.auth.js

import asyncFetch from './async-fetch';

export async function login(username, password) {

  const url = 'http://api.rest-user-api.dev/app_acceptance.php/login';

  const requestConfig = {
    method: 'POST',
    mode: 'cors',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      username,
      password
    })
  };

  const response = await asyncFetch(url, requestConfig);

  return await response.json();
}

We know this works, so we can safely assume the first thing that is going to happen in our code is that we need to await a call to asyncFetch. We also know that this call to asyncFetch should be given the url (currently hardcoded), and the requestConfig.

Ok, let's try and test this:

// /__tests__/connectivity/api.auth.js

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

      const response = await api.login('bob', 'testpass');
      expect(response).toEqual('it worked!');

    });
  });
});

Now, our test code is going to try and make a real request to api.login, with the username of bob, and password of testpass.

This won't work - not only because bob, and testpass are not valid credentials, but because our Jest tests run via node, and node natively has no concept of fetch, as we covered in the previous two videos.

At this point our test will be reporting:

FAIL  __tests__/connectivity/api.auth.js
  ● API Auth › login › has a happy path

    ReferenceError: fetch is not defined

But anyway, we don't want a real call being made. It would be slow, and we'd need an API instance to be available, and honestly that's just crazy.

Instead, we want to trick our system into thinking that a real Response object has been returned from our call to api.login, and so long as that object looks and behaves just like a real Response object would, our code can interact with it just as if it were the real deal.

This is the process of "mocking".

Mocking can be a little tricky. And often the trickier it gets, the more this is a big smelly smell that you might want to refactor your code to make testing easier. This is a big reason as to why test driven code often turns out completely differently to code written without testing in mind.

To start mocking in Jest is super easy. We just declare jest.mock('../path/to/module/to/mock/here.js'), and Jest will automatically replace the real module with a mocked equivalent.

In doing that we will get a little further, but not quite to a working state:

// /__tests__/connectivity/api.auth.js

jest.mock('../../src/connectivity/async-fetch.js');

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

      const response = await api.login('bob', 'testpass');
      expect(response).toEqual('it worked!');

    });
  });
});

Which leads to a different error:

FAIL  __tests__/connectivity/api.auth.js
  ● API Auth › login › has a happy path

    TypeError: Cannot read property 'json' of undefined

Ok, so progress.

We're now mocking 'async-fetch', which means when our real code inside the login function calls asyncFetch:

const response = await asyncFetch(url, requestConfig);

This passes through to the file src/connectivity/async-fetch.js, which has the following:

// /src/connectivity/async-fetch.js

import HttpApiCallError from '../errors/HttpApiCallError';

export default async function asyncFetch(url, requestConfig = {}) {

  const response = await fetch(url, requestConfig);

  const isSuccess = response.status >= 200 && response.status < 300;

  if (isSuccess) {
    return response;
  }

  throw new HttpApiCallError(
    response.statusText,
    response.status
  );
}

But now, Jest is just believing this call worked, and returning us an empty object in its place. We must define the configuration / shape of that object, thus 'tricking' our real implementation into continuing execution.

Don't be confused by this code. None of this will be executed. We're just pretending it will be. And if we follow the logical happy path, we expected to return response.

So, we need to tell Jest how that returned value should appear. It doesn't need to be - and won't be - a real Response object in our case. We will fake it, making our faked object look and act as if it were a real Response object.

To do this, we need to explicitly define exactly what should happen:

// /__tests__/connectivity/api.auth.js

jest.mock('../../src/connectivity/async-fetch.js');

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

      const asyncFetch = require('../../src/connectivity/async-fetch').default;
      asyncFetch.mockReturnValue({
        json: () => 'it worked!',
      });

      const response = await api.login('bob', 'testpass');
      expect(response).toEqual('it worked!');

    });
  });
});

By require'ing async-fetch, we can define our own implementation of what happens when that function is called.

In this case, we define that it should return an object. That object should have a property called json.

json contains a function, which when called, will return "it worked!".

This is just like a real Response object returned by fetch, which will have a json property, which when called will convert the response from a stream into valid JSON. Or die trying.

At this point, we have our first steps into a passing test.

Assert For Dessert

Aside from allowing us to fake the response, defining asyncFetch as a Jest mock function also gives us a bunch of additional useful goodness, primarily in the form of the .mock property.

By accessing the .mock property of our asyncFetch function after it has been used, we can ensure that - for example - the arguments passed to asyncFetch resulted in calls with our expected arguments.

// /__tests__/connectivity/api.auth.js

jest.mock('../../src/connectivity/async-fetch.js');

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

      const asyncFetch = require('../../src/connectivity/async-fetch').default;
      asyncFetch.mockReturnValue({
        json: () => 'it worked!',
      });

      const response = await api.login('bob', 'testpass');
      expect(response).toEqual('it worked!');

      const expectedUrl = 'http://api.rest-user-api.dev/app_acceptance.php/login';
      const actualUrl = asyncFetch.mock.calls[0][0];
      expect(expectedUrl).toEqual(actualUrl);

Currently we are hardcoding the URL inside our login function to our development environment. This is not something we want to do, as we will highly likely want to change the base part of that URL when our code goes into production. For the moment, let's ignore that fact - we will address it directly in the very next video - and continue on.

By accessing asyncFetch.mock.calls[0], we gain access to the first call to asyncFetch from our code.

We only make one call, so we don't need to go in asyncFetch.mock.calls[1] or higher, but if we did, we could.

Then, a sub-array on each call gives us access to the arguments in use. We can therefore assert that the first argument sent to asyncFetch should have been the url value hardcoded into our implementation.

This should be a passing test.

We could also assert that the body content we POST in matches our expectations:

// /__tests__/connectivity/api.auth.js

jest.mock('../../src/connectivity/async-fetch.js');

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  describe('login', () => {

    it('has a happy path', async () => {

      const asyncFetch = require('../../src/connectivity/async-fetch').default;
      asyncFetch.mockReturnValue({
        json: () => 'it worked!',
      });

      const response = await api.login('bob', 'testpass');
      expect(response).toEqual('it worked!');

      const expectedUrl = 'http://api.rest-user-api.dev/app_acceptance.php/login';
      const actualUrl = asyncFetch.mock.calls[0][0];
      expect(expectedUrl).toEqual(actualUrl);

      const expectedBody = JSON.stringify({
        username: 'bob',
        password: 'testpass',
      });
      const actualRequestConfig = asyncFetch.mock.calls[0][1];
      expect(expectedBody).toEqual(actualRequestConfig.body);

      expect(actualRequestConfig.method).toEqual('POST');

    });
  });
});

Cleaning Up After Oneself

It's a really good idea - speaking from personal experience here - to tidy up after yourself after each individual test has run.

This can save you a potential headscratcher whereby previous tests within the same file may inadvertantly affect later tests whereby you rely on the .mock.calls array for a previously mocked function.

To resolve this problem is really straightforward. Just add a cleanup step:

// /__tests__/connectivity/api.auth.js

jest.mock('../../src/connectivity/async-fetch.js');

import * as api from '../../src/connectivity/api.auth';

describe('API Auth', () => {

  afterEach(() => {
    jest.resetAllMocks();
  });

  describe('login', () => {

    it('has a happy path', async () => {
      // * snip *
    });
  });
});

After each test we will explicitly tell Jest to clean up after itself, alleviating any potential headaches for us, the overworked developer :)

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

Episodes