marchaos / jest-mock-extended

Type safe mocking extensions for Jest https://www.npmjs.com/package/jest-mock-extended
MIT License
845 stars 59 forks source link

How to mock return values for overloaded functions #20

Open FreifeldRoyi opened 4 years ago

FreifeldRoyi commented 4 years ago

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?

codehearts commented 4 years ago

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 commented 4 years ago

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

FreifeldRoyi commented 4 years ago

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.

regevbr commented 4 years ago

I get the same issue

tpischke-bedag commented 3 years ago

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.

shawnmclean commented 2 years ago

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

tpischke-bedag commented 2 years ago

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.

iamprathamesh commented 1 year ago

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

Mnigos commented 7 months ago

@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...