Testing with Jest

Mocking classes/modules

Classes

Let’ say we have this class:

// database.js
class Database {
  connect() {}
  save(data) {}
}

Then to mock:

import Database from "./database";

// This will mock the whole Database class, replacing all methods with jest mock functions.
jest.mock("./database");

test("should use mocked save method", () => {
  const dbInstance = new Database();

  // Mocking the save method with a specific return value
  dbInstance.save.mockReturnValue(true);

  const result = dbInstance.save({ key: "value" });

  expect(result).toBe(true);
  expect(dbInstance.save).toHaveBeenCalledWith({ key: "value" });

  // The connect method is still a mock function (but without a specific behavior).
  dbInstance.connect();
  expect(dbInstance.connect).toHaveBeenCalled();
});

Modules

Say we have the following module file:

// utils.js
export const doSomething = () => {
  // ...
};

export const fetchUserData = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

Mocked:

jest.mock("./utils", () => {
  return {
    doSomething: jest.fn(() => "mocked doSomething"),
    fetchUserData: jest.fn((userId) =>
      Promise.resolve({ id: userId, name: "Mock User" })
    ),
  };
});

test("should use mocked module functions", () => {
  expect(utils.doSomething()).toBe("mocked doSomething");
  expect(utils.doSomething).toHaveBeenCalled();

  const result = await utils.fetchUserData(123);

  expect(result).toEqual({ id: 123, name: "Mock User" });
  expect(utils.fetchUserData).toHaveBeenCalledWith(123);

});

Inline mocking versus “per test” mocking

There are two different architectures that we can use when mocking modules and classes: inline and per test mocking.

Here is the inline case:

jest.mock("./some_module.js", () => {
  return {
    someFunction: jest.fn(() => "value"),
    someFunctionWithParam: jest.fn((param) => ({
      property: param,
    })),
    someAsyncFunction: jest.fn(() => Promise.resolve("value")),
  };
});

Here is the per test case:

import { someModule } from "./some_module.js";

let someModuleMock;

someModuleMock = {
  someFunction: jest.fn(() => "value"),
  someFunctionWithParam: jest.fn((param) => ({
    property: param,
  })),
  someAsyncFunction: jest.fn(() => Promise.resolve("value")),
};

someModule.mockImplementation(() => someModuleMock);

it("should do something", () => {
  const newValue = "new value";
  someModule.someFunction.mockReturnValue(newValue);
});

The benefits of inline:

  • Inline is good because everything is set up in one place
  • Inline keeps consistency accross tests: every test case in the file will use the same mocked function unless overwritten within a test
  • It lends itself to being a global mock that can be used accross test files in a __mocks__/ directory

The benefits of per-test:

  • You can very mock implementations within the file, providing more granular control. You can redefine someModuleMock or parts of it (someModule.someFunction) throughout your test file to accomodate varied requirements between tests
  • It’s beneficial when your tests have divergent requirements, as you can perform more detailed setups and overrides for each individual test case or suite, ensuring mocks are configured exactly as required.

Overriding inline mocks

Per test mocking makes it straightforward to change the test parameters of the mocked module or class but you can also override inline mocks.

If we were using the someModule inline mock and we wanted to override the someFunction function that we have defined inline, we would first import the someFunction function and then use mockImplementation against it:

import { someFunction } from "./some_module.js";
someFunction.mockImplementation(() => "custom value");
expect(someFunction()).toBe("custom value");

// Optional: Restore the original mock implementation after the test
someFunction.mockRestore();

Note: although we are importing someFunction we are not actually importing the real function tha belongs to the module. Because Jest mocks all of its properties and methods with the inline syntax, we are actually just importing that which Jest has aready mocked, but the syntax is a bit misleading.

Applied to classes

The same approaches (with minor differences) can be used with classes:

Using inline (where the class is not the default export):

jest.mock("./SomeClass", () => {
  return {
    SomeClass: jest.fn().mockImplementation(() => {
      return {
        someFunction: jest.fn(() => "value"),
        someFunctionWithParam: jest.fn((param) => ({ property: param })),
        someAsyncFunction: jest.fn(() => Promise.resolve("value")),
        someOtherFunctionThatResolves: jest.fn().mockResolvedValue("some data"),
      };
    }),
  };
});

Using per test:

import SomeClass from "./someClass";

jest.mock("./someClass");

let someClassMock = {
  someFunction: jest.fn(() => "value"),
  someFunctionWithParam: jest.fn((param) => ({ property: param })),
  someAsyncFunction: jest.fn(() => Promise.resolve("value")),
};

// Mock class implementation
SomeClass.mockImplementation(() => someClassMock);

it("should do something", () => {
  const newValue = "new value";
  someClassMock.someFunction.mockReturnValue(newValue);
});

Check that a function has been called within another function

function toBeCalledFunction() {
  console.log("Original function called");
}

function callerFunction() {
  toBeCalledFunction();
}
test("spy on toBeCalledFunction", () => {
  const spy = jest.spyOn(global, "toBeCalledFunction"); // Replace `global` with the appropriate object/context if the function is not global
  callerFunction();
  expect(spy).toHaveBeenCalled();
  spy.mockRestore(); // Restore the original function after spying
});

Mock a function that needs to resolve to something within another function

We have two functions, one that gets data and another that processes it. We want to mock the function that gets data and return a value that the processing function can use.

async function getData() {
  // ... Fetch some data from an API or database
  return fetchedData;
}

async function processData() {
  const data = await getData();
  // ... Process the data
  return processedData;
}

The mocking part:

const mockData = { key: "value" }; // Mocked data

jest.mock("./path-to-file-where-getData-is", () => ({
  getData: jest.fn().mockResolvedValue(mockData),
}));

test("test processData function", async () => {
  const result = await processData();
  // Now, result contains the processed version of mockData
  expect(result).toEqual(/* expected processed data based on mockData */);
});

We could also combine the above with a spy to check that the getData function was called:

const getDataSpy = jest
  .spyOn(moduleContainingGetData, "getData")
  .mockResolvedValue(mockData);

const result = await processData();
expect(getDataSpy).toHaveBeenCalled();
expect(result).toEqual(/* expected processed data based on mockData */);
getDataSpy.mockRestore();

Mock a function that takes arguments

function addPrefix(str) {
  return `prefix-${str}`;
}
test("dynamic mock for addPrefix function", () => {
  const mockFunction = jest.fn((str) => `mock-${str}`);

  // Example usage of mockFunction
  const result1 = mockFunction("test");
  const result2 = mockFunction("example");

  expect(result1).toBe("mock-test");
  expect(result2).toBe("mock-example");
});

Mocking network requests

Mocking Axios

jest.mock("axios", () => ({
  get: jest.fn().mockResolvedValue(mockData),
  post: jest.fn().mockResolvedValue(mockData),
}));

Or we could implement this way:

jest.mock("axios");
axios.get.mockResolvedValue({ data: "mockedData" });
axios.post.mockResolvedValue({ data: "mockedData" });

Then we can use the mocked axios functions in our tests:

const result = await fetchData(); // the function that uses Axios `get``
expect(result).toBe("mockedGetData");

const result = await sendData({ key: "value" }); // the function tha uses Axios `post`
expect(result).toBe("mockedPostData");

mockImplementation

For more configurable cases we can use mockImplementation:

it("sends data", async () => {
  // Mock axios.post using mockImplementation
  axios.post.mockImplementation((url, data) => {
    if (data.key === "value") {
      return Promise.resolve({ data: "mockedPostData" });
    } else {
      return Promise.reject({ error: "An error occurred" });
    }
  });

  const result = await sendData({ key: "value" });

  expect(result).toBe("mockedPostData");
});

If we want to change the get and post values in different tests, we can do so by using mockImplementation:

Mocking exceptions

Again we use mockImplementation:

Say we have the following function:

// fetchData.js
import axios from "axios";

const fetchData = async (url) => {
  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    throw new Error("Error fetching data");
  }
};

export default fetchData;

We would mock the success and the error as follows:

import axios from "axios";
import fetchData from "./fetchData";

jest.mock("axios");

describe("fetchData", () => {
  it("fetches data successfully", async () => {
    axios.get.mockResolvedValue({ data: "mockedData" });

    const result = await fetchData("https://api.example.com/data");

    expect(result).toBe("mockedData");
  });

  it("throws an error when fetching fails", async () => {
    axios.get.mockImplementation(() => {
      throw new Error("API error");
    });

    // We use an asynchronous assertion here because we're expecting a promise to reject
    await expect(fetchData("https://api.example.com/data")).rejects.toThrow(
      "Error fetching data"
    );
  });
});

Parameterization

The following offers a good opportunity for parameterisation:

it("should return page for deletion from `ipages-live`", async () => {
  // preview = false, isInternal = false
  await deletePageFromS3("url", false, false);
  const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
  expect(deleteObjectCommand.input).toEqual({
    Bucket: "bbc-ise-ipages-live",
    Key: "url/index.html",
  });
});

it("should return page for deletion from `preview`", async () => {
  // preview = true, isInternal = false
  await deletePageFromS3("url", true, false);
  const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
  expect(deleteObjectCommand.input).toEqual({
    Bucket: "staff.bbc.com-preview",
    Key: "preview/url/index.html",
  });
});

...

Each time we are passing in three parameters to the deletePageFromS3 function which is the object under test. Each time there are different variations in the object that is output.

To parameterize the process rather than use repeated it blocks we can combine the input paramters and outputs into an array:

const testParams = [
  {
    preview: false,
    isInternal: false,
    bucket: "ipages-live",
    key: "url/index.html",
  },
  {
    preview: true,
    isInternal: false,
    bucket: "staff.com-preview",
    key: "preview/url/index.html",
  },
];

Then use it.each to loop through all possible parameter combinations:

it.each(testParams)(
  "should return page for deletion from %s",
  async ({ preview, isInternal, bucket, key }) => {
    await deletePageFromS3("url", preview, isInternal);
    const deleteObjectCommand = s3ClientMock.calls()[0].args[0];
    expect(deleteObjectCommand.input).toEqual({
      Bucket: bucket,
      Key: key,
    });
  }
);

This uses the %s variable to print the parameters from each test, which outputs:

  ✓ should return page for deletion from {
  preview: false,
  isInternal: false,
  bucket: 'ipages-live',
  key: 'url/index.html'
} (1 ms)
    ✓ should return page for deletion from {
  preview: true,
  isInternal: false,
  bucket: 'staff.com-preview',
  key: 'preview/url/index.html'
}