Skip to content
Advertisement

Mocking node_modules which return a function with Jest?

I am writing a typeScript program which hits an external API. In the process of writing tests for this program, I have been unable to correctly mock-out the dependency on the external API in a way that allows me to inspect the values passed to the API itself.

A simplified version of my code that hits the API is as follows:

const api = require("api-name")();

export class DataManager {
  setup_api = async () => {
    const email = "email@website.ext";
    const password = "password";
    try {
      return api.login(email, password);
    } catch (err) {
      throw new Error("Failure to log in: " + err);
    }
  };

My test logic is as follows:

jest.mock("api-name", () => () => {
  return {
    login: jest.fn().mockImplementation(() => {
      return "200 - OK. Log in successful.";
    }),
  };
});

import { DataManager } from "../../core/dataManager";
const api = require("api-name")();

describe("DataManager.setup_api", () => {
  it("should login to API with correct parameters", async () => {
    //Arrange
    let manager: DataManager = new DataManager();

    //Act
    const result = await manager.setup_api();

    //Assert
    expect(result).toEqual("200 - OK. Log in successful.");
    expect(api.login).toHaveBeenCalledTimes(1);
  });
});

What I find perplexing is that the test assertion which fails is only expect(api.login).toHaveBeenCalledTimes(1). Which means the API is being mocked, but I don’t have access to the original mock. I think this is because the opening line of my test logic is replacing login with a NEW jest.fn() when called. Whether or not that’s true, I don’t know how to prevent it or to get access to the mock function-which I want to do because I am more concerned with the function being called with the correct values than it returning something specific.

I think my difficulty in mocking this library has to do with the way it’s imported: const api = require("api-name")(); where I have to include an opening and closing parenthesis after the require statement. But I don’t entirely know what that means, or what the implications of it are re:testing.

Advertisement

Answer

I came across an answer in this issue thread for ts-jest. Apparently, ts-jest does NOT “hoist” variables which follow the naming pattern mock*, as regular jest does. As a result, when you try to instantiate a named mock variable before using the factory parameter for jest.mock(), you get an error that you cannot access the mock variable before initialization.

Per the previously mentioned thread, the jest.doMock() method works in the same way as jest.mock(), save for the fact that it is not “hoisted” to the top of the file. Thus, you can create variables prior to mocking out the library.

Thus, a working solution is as follows:

const mockLogin = jest.fn().mockImplementation(() => {
  return "Mock Login Method Called";
});

jest.doMock("api-name", () => () => {
  return {
    login: mockLogin,
  };
});

import { DataManager } from "../../core/dataManager";

describe("DataManager.setup_api", () => {
  it("should login to API with correct parameters", async () => {
    //Arrange
    let manager: DataManager = new DataManager();

    //Act
    const result = await manager.setup_api();

    //Assert
    expect(result).toEqual("Mock Login Method Called");
    expect(mockLogin).toHaveBeenCalledWith("email@website.ext", "password");
  });
});

Again, this is really only relevant when using ts-jest, as using babel to transform your jest typescript tests WILL support the correct hoisting behavior. This is subject to change in the future, with updates to ts-jest, but the jest.doMock() workaround seems good enough for the time being.

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement