Cannot spy on mocked functions of a class using a manual mock

Viewed 1017

Note: I have created a full paired down Github repo that you can download and checkout for yourself here

I am trying to mock out a non-default exported class in a module using a manual mock in the folder __mocks__ directly adjacent to the file. This works fine and I can get the mock to load correctly in my tests. However, if I export the mocked function as described here in the docs it doesn't seem to actually give me a reference to the function that the mocked class calls in the test. This means when I call

expect(getAppDetailMock).toHaveBeenCalledTimes(1);

It fails as it has not been called at all.

expect(jest.fn()).toHaveBeenCalledTimes(expected)

Expected number of calls: 1
Received number of calls: 0

My suspicion is that it has something to do with the class itself ending up with a new version of the function, and not the very same reference I am importing... I say this because I did the exact same pattern to mock out a package in my node_modules which was not a class and it worked perfectly fine.

Although you can check out the full code example here on github I have copied my files below for full clarity. Any ideas on why this is not working as I would expect?

// api/__mocks__/clients.ts

export const getAppDetailMock = jest.fn();
export const AppsClient = jest.fn().mockImplementation(() => {
  return {
    getAppDetail: getAppDetailMock,
  };
});

// App.test.js

import React from "react";
import {
  render,
  screen,
  waitForElementToBeRemoved,
} from "@testing-library/react";
import App from "./App";
import { getAppDetailMock } from "./api/__mocks__/clients";

jest.mock("./api/clients");

describe("<App /> ", () => {
  beforeEach(() => {
    getAppDetailMock.mockReset();
  });
  test("Should show not found message if app does not exist", async () => {
    const appId = "8500f5dd-8b41-4cb8-95fa-246b1f25855b";
    getAppDetailMock.mockResolvedValue(null);
    render(<App appId={appId} />);
    await waitForElementToBeRemoved(() => screen.getByText("Loading"));
    expect(getAppDetailMock).toHaveBeenCalledTimes(1);
    expect(getAppDetailMock).toHaveBeenCalledWith(appId);
    expect(screen.getByText(`App not found found`)).toBeInTheDocument();
  });
  test("Should show not found message if app does not exist", async () => {
    const appId = "8500f5dd-8b41-4cb8-95fa-246b1f25855b";
    getAppDetailMock.mockResolvedValue({ appId });
    render(<App appId={appId} />);
    await waitForElementToBeRemoved(() => screen.getByText("Loading"));
    expect(getAppDetailMock).toHaveBeenCalledTimes(1);
    expect(getAppDetailMock).toHaveBeenCalledWith(appId);
    expect(
      screen.getByText(`App found with id '${appId}'`)
    ).toBeInTheDocument();
  });
});

// App.tsx

import React, { useState, useEffect } from "react";
import "./App.css";

import { AppsClient, AppDetailDto } from "./api/clients";

type AppProps = {
  appId: string;
};

const App: React.FC<AppProps> = ({ appId }) => {
  const [hasLoaded, setHasLoaded] = useState(false);
  const [appDetail, setAppDetail] = useState<AppDetailDto | null>(null);

  useEffect(() => {
    const getAppDetail = async (appId: string) => {
      try {
        setHasLoaded(false);
        const result = await new AppsClient().getAppDetail(appId);
        setAppDetail(result);
        setHasLoaded(true);
      } catch (error) {}
    };
    if (!hasLoaded) {
      getAppDetail(appId);
    }
  }, [appId, hasLoaded]);

  if (!hasLoaded) {
    return <>Loading</>;
  } else if (!appDetail) {
    return <>App not found found</>;
  }

  return <>App found with id '{appDetail.appId}'</>;
};

export default App;

// api/clients.ts

import { BaseClient } from "./baseClient";

export interface AppDetailDto {
  appId: string;
}

export class AppsClient extends BaseClient {
  getAppDetail(appId: string): Promise<AppDetailDto> {
    return Promise.resolve({ appId });
  }
}

2 Answers

Try checking to see if your getAppDetail has been called instead.

so something more like: expect(getAppDetail).toHaveBeenCalledTimes(1);

Or, if you can, pull in the entire mocked component and check that it's specific getAppDetail has been called, like: expect(AppsClient.getAppDetail).toHaveBeenCalledTimes(1);

Your suspicion is probably correct... the fact that you're importing a separate instance of the getAppDetailMock, that instance of it won't have been called, as it's a separate import not related to the AppsClient.getAppDetail mock.

Try import direct from ./api/clients with @ts-ignore

// @ts-ignore
import { getAppDetailMock } from "./api/clients";

or

import * as Clients from './api/clients';
import * as ClientsMock from './api/__mocks__/clients';

jest.mock("./api/clients");

const {
  getAppDetailMock
} = (Clients as unknown) as typeof ClientsMock;

Related