jest-mock-extended icon indicating copy to clipboard operation
jest-mock-extended copied to clipboard

How to mock return values for overloaded functions

Open FreifeldRoyi opened this issue 5 years ago • 11 comments

I'm trying to mock an overloaded function. More precisely NestJS' ConfigService.get but mockService.get.calledWith expects the 2 param method while I need the one param.

How to resolve this issue?

FreifeldRoyi avatar Mar 30 '20 16:03 FreifeldRoyi

I ran into the same issue when mocking Fingerprintjs2; Fingerprint2.get.mockImplementation expected the one argument signature, but I needed to implement the two argument version. This is what it took to make it work (the commented lines are the important ones), perhaps this could be clearly documented or streamlined somehow?

import {MockProxy, mockReset} from 'jest-mock-extended';
import Fingerprint2 from 'fingerprintjs2';
jest.mock('fingerprintjs2');

type FingerprintCallback = (data: Fingerprint2.Component[]) => void;

// Signature for `Fingerprint2.get` to mock (we want the one with two args)
type FingerprintGetWithTwoArgs = (
  opt: Fingerprint2.Options,
  callback: FingerprintCallback
) => void;

const MockFingerprint2 = Fingerprint2 as MockProxy<typeof Fingerprint2>;

// Reset the mock before each test to remove any custom implementations
beforeEach(() => mockReset(MockFingerprint2));

it('passes generated fingerprint to callback', () => {
  const testData = [{key: 'foo', value: 'bar'}];

  // Cast the mocked function to the signature we want and assign our own `jest.fn`
  (Fingerprint2.get as FingerprintGetWithTwoArgs) = jest.fn(
    (opt: Fingerprint2.Options, callback: FingerprintCallback) => {
      callback(testData);
    },
  );

  expect.assertions(1);
  Fingerprint2.get({}, (data: Fingerprint2.Component[]) => {
    // Ta-da! We got the two-argument version to pass our test data back
    expect(data).toBe(testData);
  });
});

The gist is that you cast your mocked function as the signature you want, then assign it as a jest.fn or something

codehearts avatar Apr 06 '20 19:04 codehearts

I should mention that I only realized this because the type of Fingerprint2.get was this beast:

CalledWithMock<void, [(components: Component[]) => void]> & {
  (options: Options, callback: (components: Component[]) => void): void;
  (callback: (components: Component[]) => void): void;
}

You can see the CalledWithMock that provides mockImplementation is for the one argument signature, but the type intersects with the two-argument function so you can cast to that

codehearts avatar Apr 06 '20 19:04 codehearts

Wow... I tried to add undefined as the second parameter and it worked. But I feel like it should be clear in tests that the the tested code is calling one function and not the other, and currently it is not.

FreifeldRoyi avatar Apr 07 '20 08:04 FreifeldRoyi

I get the same issue

regevbr avatar Apr 11 '20 17:04 regevbr

These workarounds are very verbose and hard-to-read, and somewhat defeat the purpose of using jest-mock-extended. Since most of the mocks we use are for OpenAPI generated services which make generous use of overloading, this issue negates much of the value of jest-mock-extended in our projects. Worse, there is no decent documentation about how to use this workaround with mockReturnValue, etc.

tpischke-bedag avatar Dec 11 '20 09:12 tpischke-bedag

@tpischke-bedag did you switch to a better lib?

shawnmclean avatar Dec 06 '21 23:12 shawnmclean

We are still using the library, but only in a very limited fashion. We've mostly switched to implementing our own simple stubs to avoid these issues.

tpischke-bedag avatar Dec 07 '21 07:12 tpischke-bedag

are we still not able to use jest for overloaded methods? can someone help with a demo code for any work-around?

iamprathamesh avatar Jan 28 '23 06:01 iamprathamesh

@iamprathamesh

SpotifyAlbumsService

export class SpotifyAlbumsService {
  public getAlbum(id: string, adapt: false): Promise<SdkAlbum>
  public getAlbum(id: string, adapt: true): Promise<Album>

  async getAlbum(id: string, adapt = false) {
    this.spotifySdk = SpotifyApi.withClientCredentials(
      this.configService.get<string>(Environment.SPOTIFY_CLIENT_ID)!,
      this.configService.get<string>(Environment.SPOTIFY_CLIENT_SECRET)!
    )

    const data = await this.spotifySdk.albums.get(id)

    return adapt ? this.adaptersService.albums.adapt(data) : data
  }
}

test

  test('should create album from external id', async () => {
    const getAlbumSpy = (
      jest.spyOn(spotifyAlbumsService, 'getAlbum') as unknown as MockInstance<
        [id: string, adapt: false],
        Promise<SdkAlbum>
      >
    ).mockResolvedValue(sdkAlbumMock)
  })

I wish there is a better way to do that...

Mnigos avatar Mar 20 '24 12:03 Mnigos