Home / Blog / Test Doubles
Test Doubles

Test Doubles

Alex Jover Morales
Alex Jover Morales
Updated: May 15th 2022
This entry is part 3 of 5 in the series Testing like a Pro in JavaScript

Test doubles, spies, and mocking, all sound intriguing, but what do they actually mean? Do I really need to spy on my code, or mock it? Let's find out!

Dealing with dependencies

When we build an application, we start to structure the code, separating it in different components and modules that we import and reuse all over the application.

When a module imports another module, the last one becomes a dependency of the first. For example, imagine we have a student-utils.js module with a validateStudent function that checks the existence of the student id:

// student-utils.js
export const validateStudent = student => {
  return Boolean(student.id && student.id > 0);
}

And we use it from another module student.js:

// student.js
import { validateStudent } from "./student-utils";

export default function createStudent(id, name) {
  const student = { id, name };
  if (!validateStudent(student)) {
    throw new Error("Invalid student: it doesn't have an id");
  }
  return student;
}

Then student-utils becomes a dependency of student, meaning that whenever we use student, implicitly we’re using student utils as well.

This dependency seems logical and may look harmless, but dependencies often can make a module hard to test. The question is…

When does a dependency make testing hard?

Simple: when a dependency has side effects. With side effects I mean a function that is calling something external, such as an API, a Database, some kind of state, etc.

The last example is simple and pure since validateStudent just takes a student and performs some checks on its id. Thus it has no side effects.

But imagine that we have a fetchStudent function, that calls an API:

// student-utils.js
const baseUrl = "https://jsonplaceholder.typicode.com/users/";

export const fetchStudent = async id => {
  const student = await fetch(baseUrl + id);
  return student.json();
};

When we’re doing unit or integration tests, we want to test a unit or part of the system, avoiding any extra dependencies. The fetchStudent function has a side effect since it relies on an external call, so it will break or make our tests inconsistent.

That’s where test doubles come into play.


What’s a test double?

Think of a stunt double: a person who stands in for an actual actor when they need to perform some kind of dangerous stunt.

Test doubles relate to stunt doubles: they are objects that replace the original test item in order to make testing easier and decouple dependencies.

They’re used in unit tests, and often in integration tests, depending on the case. What we want to achieve is to replace the dependencies that perform side effects with a test double that shares the same interface.

There are different types of test doubles, each of them with different purposes: spies, stubs and mocks.

Spies

Spies are used to observe the test items and keep a count on how many times something has been called, but they don’t alter the implementation.

They are commonly used for those cases where a dependency doesn’t need to be replaced due to not having any side effects.

That’s perfectly the case of the validateStudent function from the previous example. It doesn’t make sense to replace its implementation. Likely what you want to test from a student is that, when it’s created, it is validated.

Let’s implement a first test of the create student, for now without using any kind of spy, in order to assert that the student is created correctly:

import createStudent from "./student";

describe("Student", () => {
  it("can create a student passing an id and name", () => {
    const student = createStudent(2);
    expect(student.id).toBe(2);
  });
});

The test basically asserts that the user has been created and has the correct id. That’s the correct case, but for the incorrect test we have to check that an exception has been triggered.

Jest comes with the toThrow matcher for exceptions, which is perfect for our use case. It accepts an argument to assert the specific error as well:

it("throws an error if id is not passed", () => {
  expect(() => createStudent()).toThrow();

  // You can be more specific
  expect(() => createStudent()).toThrow(Error);
  const msg = "Invalid student: it doesn't have an id";
  expect(() => createStudent()).toThrow(msg);
});

Probably you’ve realised that just with this test we’re implicitly testing the validateStudent function: the student is valid if the exception is thrown, and not valid in the other case. You could test the validateStudent function separately, however, this is one of those cases where I’d say it’s not necessary by the fact that createStudent already tests it.

However, if you want to explicitly check that the createStudent function calls validateStudent underneath, we can use jest.spyOn in order to create a spy on validateStudent.

The spyOn function has the signature spyOn(object, propName), so we’ll need to import all student utils and spy on the validateStudent property:

import createStudent from "./student";
import * as utils from "./student-utils";

describe("Student", () => {
  // ...
  it("calls the validateStudent function", () => {
    jest.spyOn(utils, "validateStudent");
    createStudent(1, "Aaron");
    expect(utils.validateStudent).toBeCalled();
  });
});

I’ve used the toBeCalled matcher on the utils.validateStudent, which asserts that a function has been called. You can be more specific by defining how many times it’s been called or with which parameters:

expect(utils.validateStudent).toBeCalledWith({ id: 1, name: "Aaron" });
expect(utils.validateStudent).toHaveBeenCalledTimes(1);

We’re still missing something. What if we create another test calling the same function, and we check that it has been called exactly one time on each test?

describe("Student", () => {
  it("calls the validateStudent function", () => {
    const spy = jest.spyOn(utils, "validateStudent");
    createStudent(1, "Aaron");
    expect(spy).toHaveBeenCalledTimes(1);
  });

  it("calls the validateStudent function", () => {
    const spy = jest.spyOn(utils, "validateStudent");
    createStudent(2, "Peep");
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

Reset mock state

You probably expect that these tests passes, but the thing is that the second will fail with the error:

Expected mock function to have been called one time, but it was called two times.

That’s because we’re already setting the spy in the first test, and even though in the second we’re calling jest.spyOn again, the validateStudent function is already spied and the call counters are not reset, accumulating the calls.

For that reason, is very important to reset the spy counters after each test. Jest gives us three functions for that:

  • mockClear: only resets the counters
  • mockReset: resets the mock to its original state, including implementations
  • mockRestore: like mockReset but removes the mock function

Keep in mind that Jest generalises mocks, stubs and spies into just mock functions.

For our case, both mockClear and mockRestore would work:

it("calls the validateStudent function", () => {
  const spy = jest.spyOn(utils, "validateStudent");
  createStudent(1, "Aaron");
  expect(spy).toHaveBeenCalledTimes(1);
  spy.mockClear();
});

it("calls the validateStudent2 function", () => {
  const spy = jest.spyOn(utils, "validateStudent");
  createStudent(2, "Peep");
  expect(spy).toHaveBeenCalledTimes(1);
  spy.mockClear();
});

Jest Hooks

You can see that it could be very repetitive to create and clear a mock function. Luckily, Jest has hooks we can use to perform any action at different points: beforeEach, afterEach, beforeAll, afterAll. They’re scope is the describe function where they’re located.

For our case, we can use afterEach to clean the spy after each test:

import createStudent from "./student";
import * as utils from "./student-utils";

describe("Student", () => {
  const spy = jest.spyOn(utils, "validateStudent");

  afterEach(() => {
    spy.mockClear();
  });

  it("calls the validateStudent function", () => {
    createStudent(1, "Aaron");
    expect(spy).toHaveBeenCalledTimes(1);
  });

  it("calls the validateStudent2 function", () => {
    createStudent(2, "Peep");
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

And as you can see, we avoid repeating ourselves.

In this case a beforeAll hook is not necessary to set up the spy, but there are more complex cases where it can be. For example, let’s say that you have to perform some asynchronous operation before running the tests, such as cleaning a database.

For those cases, both the hooks and the it test functions can return a promise and they’ll wait to be resolved. For example:

beforeAll(() => {
  return Promise.resolve("Dummy example");
});

You can even use the async/await syntax for a nicer and more sync-like syntax. The following example is equivalent to the previous:

beforeAll(async () => {
  await Promise.resolve("Dummy example");
});

Mocks and Stubs

The difference between mocks and stubs is controversial and not clear. They both achieve the same thing: to replace an actual implementation with a fake one, in order to decouple the dependencies of a test.

Some say that stubs implementations are static, while mocks are dynamic and per test. Anyway, from now on we’ll refer to them both as mocks.

Before getting into mocks, let’s implement a fetch function in student.js that uses the fetchStudent function from student-utils.js:

import { validateStudent, fetchStudent } from "./student-utils";

export default function createStudent(id, name) {
  const student = { id, name };
  if (!validateStudent(student)) {
    throw new Error("Invalid student: it doesn't have an id");
  }
  // Add the fetch function before returning the object
  student.fetch = () => fetchStudent(student.id);
  return student;
}

First we’re going to test the student.fetch function without mocking in order to see the problem of the dependencies and side effects. Then we’ll go through the dependency injection technique and mocking in order to avoid those problems.

Start by adding a test in student.test.js that calls the fetch function and asserts some data:

describe("student.fetch", () => {
  it("returns data", async () => {
    const student = createStudent(1);
    const data = await student.fetch();
    expect(data.username).toBe("Bret");
  });
});

The thing is that if you run the test, it will fail because it doesn’t find the fetch function on student-utils.js.

Prerequisite: Jest setup and fetch polyfill

The problem is that Jest runs on top of JSDom, a node environment that emulates a browser. The Fetch API is a web standard implemented in modern browsers and is not available in Jest.

In order to make it work, we need to use a polyfill, which is a library that implements a missing functionality from modern browsers. We’ll use the isomorphic-fetch since it works both on Node.js and the browser. Start by installing it:

npm install --save-dev isomorphic-fetch

Even though Jest works out of the box, it has lots of configuration options available. They can be set by adding a "jest" property to the package.json or creating a jest.config.js file in case you need more flexibility. We’ll go for the package.json approach since is more simple and doesn’t require extra files.

Among all the Jest options, we can use the setupScripts and pass script paths. This is useful for cases like this one where you need to do some preparations before running the tests. Let’s set it in the package.json passing a setupTests file:

{
  // ...
  "jest": {
    "setupFiles": ["./setupTests"]
  }
}

Then create that setupTests.js in the root of the project and assign isomorphic-fetch to the global.fetch property:

global.fetch = require("isomorphic-fetch");

Note: global is the equivalent to window in a Node.js environment

If we run the tests again, they shouldn’t crash because of not finding a fetch function anymore.

Why mocking?

Back to the test:

it("returns data", async () => {
  const student = createStudent(1);
  const data = await student.fetch();
  expect(data.username).toBe("Bret");
});

By the date of writing, this test is passing for me. However, there are several reasons why this test is wrong:

  • It uses the fetchStudent util, which is using the real fetch function under the hood.
  • It relies on the internet.
  • The response from the server is totally unpredictable. The username can change any time.

For these reasons, this test is not reliable at all. And most importantly, the purpose of the test is to make sure student.fetch is called and returns data, but we don’t care about anything else. That applies to most test when unit testing: they should focus on testing one particular case, avoiding any extra dependency.

Dependency Injection

Aside from mocking, there are other techniques we can use for dealing with dependencies.

Dependency Injection is a design pattern that tells us to pass a dependency to an object from the outside. In that way, that object doesn’t have that dependency hard-coded. In that way, that dependency is the problem of “someone else” being the user of that object

This might be easier to understand with an example: Right now, student.js depends on student-utils.js for the fetchStudent function. If we pass the fetchStudent function from the outside, for example as a parameter of the createStudent function, then we don’t have that dependency anymore:

import { validateStudent } from "./student-utils";
export default function createStudent(id, name, fetchStudent) {
  const student = { id, name };
  if (!validateStudent(student)) {
    throw new Error("Invalid student: it doesn't have an id");
  }
  student.fetch = () => fetchStudent(student.id);
  return student;
}

As you can see, we’re providing it in the 3rd parameter of createStudent. We could apply the same for validateStudent, but that function is not problematic so we can ignore it.

Since we broke that dependency, we can now test it easily by passing our own function to createStudent:

describe("student.fetch", () => {
  const fetchStudent = () => Promise.resolve({ username: "Bret" });

  it("returns data", async () => {
    const student = createStudent(1, "", fetchStudent);
    const data = await student.fetch();
    expect(data.username).toBe("Bret");
  });
});

Now we made this test reliable: it doesn’t depends on internet nor in any external data source and it achieves its goal. And the best thing, we didn’t need to mock anything at all, we only needed a technique.

The tradeoff with dependency injection is that it requires more boilerplate. Now every time we want to call createStudent, we must pass the fetchStudent function. You can always create abstractions that connect the dependencies. At the end, this is a technique, use it when it’s helpful for your case.

Using Mocks

Jest has a very powerful feature: mock functions. To create one, it’s as simple as getting the result from jest.fn():

const myMock = jest.fn();

It shares the same callable API from spies, so we can check whether it’s been called:

myMock("hiyaa");
expect(myMock).toBeCalledWith("hiyaa");
myMock.mockClear();

Additionally, we can define its implementation by passing a function as a parameter to jest.fn:

const myMock = jest.fn(() => 12);
expect(myMock()).toBe(12);

And if you need more grained control, you could use the mockImplementationOnce and mockImplementation functions. The first defines a one-call implementation, while the other the default one:

const myMock = jest.fn()
  .mockImplementationOnce(() => 12)
  .mockImplementationOnce(() => "Caribu cola")
  .mockImplementation(() => "Miau");

expect(myMock()).toBe(12);
expect(myMock()).toBe("Caribu cola");

// From here, it'll be always Miau
expect(myMock()).toBe("Miau"); 
expect(myMock()).toBe("Miau");

We could implement the previous example of the fetchStudent using a mock function. That we can check its calls:

const fetchStudent = jest.fn(() => Promise.resolve({ username: "Bret" }));

it("returns data", async () => {
  const student = createStudent(1, "", fetchStudent);
  const data = await student.fetch();
  expect(data.username).toBe("Bret");
  expect(fetchStudent).toBeCalledWith(1);
});

You can see that mock functions are quite powerful, but we still don’t know how to mock a module without using dependency injection.

Mocking modules

Let’s go back in time to the code before using dependency injection:

import { validateStudent, fetchStudent } from "./student-utils";

export default function createStudent(id, name) {
  // ...
  student.fetch = () => fetchStudent(student.id);
}

Jest provides a nice way to remove dependencies without using dependency injection: mocking modules. We can mock a module by using the jest.mock function, which takes the module path to mock as its first argument.

jest.mock mocks the whole module, and since we just want to mock the fetchStudent function, we need to refactor it and move it to a student-service.js module. We’ll return an object, renaming it to get to conform to the HTTP verbs convention:

// student-service.js
const baseUrl = "https://jsonplaceholder.typicode.com/users/";

export default {
  async get(id) {
    const result = await fetch(baseUrl + id);
    return result.json();
  }
};

And update the student.js accordingly:

import { validateStudent } from "./student-utils";
import studentService from "./student-service";

export default function createStudent(id, name) {
  // ...
  student.fetch = () => studentService.get(student.id);
}

The tests should be working the same way previous to the refactor.

Now, we can use jest.mock for the students-service module in order to mock it:

// student.test.js
jest.mock("./student-service");
// ...
describe("student.fetch", () => {
  it("returns data", async () => {
    const student = createStudent(1, "");
    const data = await student.fetch();
    expect(data.username).toBe("Brat");
  });
});

Note: jest.mock is hoisted at the top of the file, so let’s always write it on the top to avoid confusion

Just like that, jest.mock automatically mocks each property returned on the student-service module to an empty jest.fn implementation. In other words, from that point the student-service implementation will be:

{
  get: jest.fn()
}

That’s why, if we run the test it will fail, since student.fetch() won’t return any value. However, we can import the student-service and check the get calls, since it’s a mock function:

jest.mock("./student-service");
import studentService from "./student-service";
// ...
it("returns data", async () => {
  const student = createStudent(1, "");
  const data = await student.fetch();
  expect(studentService.get).toBeCalledWith(1);
});

If we need more fine-grained implementation, jest.mock can receive as a second argument a factory function to have a custom mock implementation of that module. For example, let’s make the get function to return a promise resolving to an arbitrary value:

jest.mock("./student-service", () => ({
  get() {
    return Promise.resolve("Yay");
  }
}));

it("returns data", async () => {
  const student = createStudent(1, "");
  const data = await student.fetch();
  expect(data).toBe("Yay");
});

At this point you’ve seen several ways to apply module mocking. They all allows you to remove your code dependencies in an easy way.

As you can imagine, in a real app you’ll have lots of these services, and in most cases you don’t care about the implementation of the fetch function, you just don’t want it to be called.

What if I told you, that there is even an easier and more concise way?

Mock files

Jest, once again, stands out for its mocking capabilities. It allows to define mock files where you can define mocked implementations of any module.

Mocking files works by convention: you just need to create a *mocks* folder at the same level of the module you want to mock. For instance, if your module is in src/student.js you need to create it on src/mocks/student.js. Jest will detect that the student module has a mock when we call jest.mock("./student.js"), and it will be applied.

In the previous example, we can create a mocks/student-service.js file in order to separate the mock code and reuse it in all tests we need to:

// __mocks__/student-service.js
export default {
  get: jest.fn(id => Promise.resolve(id))
};

And remove the custom implementation from the test:

// student.test.js
jest.mock("./student-service");
// ...
it("returns data", async () => {
  const student = createStudent(1, "");
  const data = await student.fetch();
  expect(data).toBe(1);
});

In medium to large codebases, where you have lots of modules with more complex and extended implementations, this avoids a lot of extra setup for all the tests and separates the mock code from the test and source code.

We can improve this example even further. Remember that is not all about mocking, but software development is more about techniques and design patterns.

In a real application it is common to implement your own API client abstraction for the fetch function, axios or whatever you use. That will allow us to automatically mock that api module for all the tests, so we don’t need to mock every service that calls an API.

Let’s start illustrating the example by creating an api.js file:

// api.js
export default {
  async get(url) {
    const result = await fetch(url);
    return result.json();
  }
};

Then, student-service will import it and use it:

// student-service.js
import api from "./api";
const baseUrl = "https://jsonplaceholder.typicode.com/users/";

export default {
  get(id) {
    return api.get(baseUrl + id);
  }
};

And here’s the cool part: we can globally mock it from the setupTests.js file we’ve created before. It totally makes sense, since api.js is a file that we’ll always want it mocked.

// setupTests.js 
global.fetch = require("isomorphic-fetch");
jest.mock("./src/api");

In that way, you don’t need to mock the service nor mock the api from the tests anymore, becoming cleaner and more concise:

// student.test.js
import createStudent from "./student";
import * as utils from "./student-utils";

describe("Student", () => {
  // ...
  describe("student.fetch", () => {
    it("returns data", async () => {
      const student = createStudent(1, "");
      const data = await student.fetch();
      expect(data).toBe("https://jsonplaceholder.typicode.com/users/1");
    });
  });
});

Keep in mind that you can still rewrite the implementation for a particular test. So if in another test you have the uncommon need to have a custom implementation of the api, you can still do it:

// student.test.js
jest.mock("./student-service", () => ({
  get: jest.fn(() => "Something different meh :S")
}));

Start learning Vue.js for free

Alex Jover Morales
Alex Jover Morales
Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.

Comments

Latest Vue School Articles

From Vue.js Options API to Composition API: Is it Worth it?

From Vue.js Options API to Composition API: Is it Worth it?

Explore the technicalities of transitioning from Options API to Composition API in Vue.js. Discover if migrating your app is worth the effort in our detailed guide
Mostafa Said
Mostafa Said
What’s New in Nuxt 4

What’s New in Nuxt 4

Have anxiety about a new major version of Nuxt coming out? Worried about a big migration project? Don’t worry about it, a peaceful and easy upgrade is literally one of the features of Nuxt version 4.
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.