By the end of this post, you'll be able to use Jest to correctly test out your code containing
http requests, file access methods, database calls or any other kind of side effects the production code might create.
Also you'll learn how to deal with other kinds of similar issues, where you need to avoid actually calling out methods or modules( eg database calls).

Mocking http requests

Let's suppose that we have a piece of code that use XMLHttpRequest to perform network activity, something along these lines:

const API_ROOT = "http://jsonplaceholder.typicode.com";
class API {
getPosts() {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", `${API_ROOT}/posts`);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText);
if (resp.error) {
reject(resp.error);
} else {
resolve(resp);
}
}
};
xhr.send();
});
}
}

export default new API();

This module is used in various places within the codebase and could potentially incorporate many more methods for accessing the application's API.

How do we actually test out this method ?

First of all, let's define what this method does.

getPosts() should make an API call to retrieve the lists of posts in the blog and should return a Promise resolving to the list of posts

What we want to avoid is actually making that API call in our tests.

Why ?

As you can see, there are plenty of reasons for which we want to avoid working directly in unit tests with actual network requests.

Mocking out XMLHttpRequest

The solution goes back to using a proper mock for XMLHttpRequest object, that intercepts the calls made to it and fakes the behavior.

If you don't remember exactly what mocks are and how they're used you can checkout using spies and fake timers with Jest and the official Jest docs on mocks.

Let's have a naive attempt at creating the very first test( suppose we have an API.spec.js file):

import API from "./API.js";
const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify([
{ title: "test post" },
{ tile: "second test post" }
])
};
const oldXMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = jest.fn(() => mockXHR);

describe("API integration test suite", function() {
test("Should retrieve the list of posts from the server when calling getPosts method", function(done) {
const reqPromise = API.getPosts();
mockXHR.onreadystatechange();
reqPromise.then(posts => {
expect(posts.length).toBe(2);
expect(posts[0].title).toBe("test post");
expect(posts[1].title).toBe("second test post");
done();
});
});
});

Let's understand what happens here, step by step:

const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify([
{ title: "test post" },
{ tile: "second test post" }
])
};

We start by creating a fake XHR object.
The actual open and send methods are just functions that don't do anything.
We also set readyState to 4(which is typically used for checking if the request has completed) and a responseText that suits what we are trying to test.

We can simulate any API response we want by giving responseText a proper text value.

const oldXMLHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = jest.fn(() => mockXHR);

Next, we are backing up the built-in XMLHttpRequest object, replacing it with a function that returns our mocked object.
It's a good idea to back-up the real XMLHttpRequest object because at the end of the test we should cleanup after ourselves, leaving the environment in the same state that we've found it.

As such whenever new XMLHttpRequest is being called the mockXHR object will get returned.

And , finally we this setup in place it's pretty easy to unit test getPosts:

const reqPromise = API.getPosts();
mockXHR.onreadystatechange();

Basically just call the API and then simulate that the response arrived by calling onreadystatechange. When onreadystatechange is invoked we are actually invoking the state changed callback that is set within Api.js:

xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
const resp = JSON.parse(xhr.responseText);
if (resp.error) {
reject(resp.error);
} else {
resolve(resp);
}
}
};

Since the mocked XHR object has readyState set to '4', the Promise object will be immediately resolved or rejected depending on the response.

Regarding the assertions we are verifying that the Promise being returned from getPosts() has
the actual JSON response as the resolved value.

We do this by checking that the number of items is correct and that each item is what is supposed to be.

expect(posts.length).toBe(2);
expect(posts[0].title).toBe("test post");
expect(posts[1].title).toBe("second test post");

Taking it a step further

There are several improvements we can make to the approach, that will make it more flexible and easier to use.

First of all, let's create a factory function for mock XHR objects that allows us to create new mock XHRs very easily and to, optionally, specify the response of the object.

const createMockXHR = responseJSON => {
const mockXHR = {
open: jest.fn(),
send: jest.fn(),
readyState: 4,
responseText: JSON.stringify(responseJSON || {})
};
return mockXHR;
};

Next up, let's make a new XHR object for each unit test. When unit testing, the last thing we want is to have shared state across the unit tests which lead to unpredictable and hard to debug tests.

The actual test suite will look like this:

describe("API integration test suite", function() {
const oldXMLHttpRequest = window.XMLHttpRequest;
let mockXHR = null;

beforeEach(() => {
mockXHR = createMockXHR();
window.XMLHttpRequest = jest.fn(() => mockXHR);
});

afterEach(() => {
window.XMLHttpRequest = oldXMLHttpRequest;
});

test("Should retrieve the list of posts from the server when calling getPosts method", function(done) {
const reqPromise = API.getPosts();
mockXHR.responseText = JSON.stringify([
{ title: "test post" },
{ title: "second test post" }
]);
mockXHR.onreadystatechange();
reqPromise.then(posts => {
expect(posts.length).toBe(2);
expect(posts[0].title).toBe("test post");
expect(posts[1].title).toBe("second test post");
done();
});
});
});

Also, we are cleaning up after ourselves, after each test, by mocking the XMLHttpRequest in beforeEach and restoring the native XMLHttpRequest object in afterEach.

beforeEach(() => {
mockXHR = createMockXHR();
window.XMLHttpRequest = jest.fn(() => mockXHR);
});

afterEach(() => {
window.XMLHttpRequest = oldXMLHttpRequest;
});

Another benefit that we get is that we can test out different scenarios, very easily.
Let's suppose we want to add another test, simulating the API returning an error:

test("Should return a failed promise with the error message when the API returns an error", function(done) {
const reqPromise = API.getPosts();
mockXHR.responseText = JSON.stringify({
error: "Failed to GET posts"
});
mockXHR.onreadystatechange();
reqPromise.catch(err => {
expect(err).toBe("Failed to GET posts");
done();
});
});

Did you noticed how easy is to mock different responses from the API ?

How about mocking file system/DB calls and other side effects

We can very easily expand this technique that we applied to HTTP requests to also cover other types
of side effects.

Let's assume that we have a FileSystem component that has a method for reading out a file and parsing it as JSON:

import fs from "fs";
export default class FileSystem {
parseJSONFile(file) {
const content = String(fs.readFileSync(file));
return JSON.parse(content);
}
}

We would like to test parseJSONFile() method, but we want to avoid actually creating the file and reading it's contents from disk.

Our test suite can look like this:

jest.mock("fs", () => ({
readFileSync: jest.fn()
}));

import FileSystem from "./FileSystem.js";
import fs from "fs";

describe("FileSystem test suite", function() {
test("Should return the parsed JSON from a file specified as param", function(done) {
const fileReader = new FileSystem();
fs.readFileSync.mockReturnValue('{ "test": 1 }');
const result = fileReader.parseJSONFile("test.json");
expect(result).toEqual({ test: 1 });
done();
});
});

Let's go throught this step by step:

jest.mock("fs", () => ({
readFileSync: jest.fn()
}));

jest.mock permits us to mock any module we might have, including the ones built in NodeJS and have a factory function as the second arg, returning the mock return value.

In our case whenever we have const fs = require('fs'); or import fs from 'fs' in code the imported value will actually be the object that we returned from the factory function:

{
readFileSync: jest.fn();
}

It's important that the jest.mock call is invoked before the usage of fs.readFileSync .

Next, we instantiate the component we want to test const fileReader = new FileSystem(); and instruct the readFileSync spy to return a certain, pre-made, string:

fs.readFileSync.mockReturnValue('{ "test": 1 }');

Checkout mockReturnValue in Jest docs if you need more info on how it operates.

Finally, we verify that what comes out of parseJSONFile is the parsed JSON value:

expect(result).toEqual({ test: 1 });

This concludes our introduction to mocking the http calls and the file system calls.

Let's us know what you think in comments and if you like it follow us on Twitter or subscribe to our newsletter to stay up to date!