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.