TypeScript+Jest - how to use jest.spyOn with generic TypeScript methods?

Viewed 6190

I have the following TypeScript object defined:

// HttpClient.ts
export type HttpResponse<T> = Response & {
  data?: T;
}

async function get<T>(url: string, args: RequestInit = {}): Promise<HttpResponse<T>> {
  return fetchRequest<T>(url, {...args, method: 'get'}) // fetchRequest returns value of type Promise<HttpResponse<T>>
}

export default {get} // default export represents an HttpClient object

I also have another file, getFeatureFlag.ts, that uses this HttpClient in its implementation:

// getFeatureFlag.ts
import HttpClient from '../httpClient'

export type FetchResponse<T = any, E = any> = {
  data: Nullable<T>;
  error: Nullable<E>;
}

export type FeatureFlag = {
  toggleActive: boolean; // true/false denotes show/hide feature
}

export async function getFeatureFlag(flagName: string): Promise<FetchResponse<boolean, string>> {
  const response = await HttpClient.get<FeatureFlag>(`/v1/toggles/${flagName}/`)

  // ... rest of the implementation ...
}

I'm in the process of writing unit tests (using Jest) for this file under getFeatureFlag.test.ts and I have the following snippet within it:

// getFeatureFlag.test.ts
import HttpClient, {HttpResponse} from '../httpClient'
import {getFeatureFlag, FLAG_STORE, FeatureFlag} from '../index'

describe('getFeatureFlag', () => {
  beforeEach(async () => {
    // vv === THIS LINE IS WHAT THIS POST IS ABOUT === vv
    jest.spyOn<HttpResponse<FeatureFlag>, 'get'>(HttpClient, 'get').mockImplementationOnce(() => ({
      ok: true,
      data: {
        toggleActive: true,
      },
    }))

    result = await getFeatureFlag(flagName)
  })

  it('then perform api request for feature flag data', () => {
    expect(HttpClient.get).toHaveBeenCalledWith('/v1/toggles/featureFlag/')
  })
})

From within the @types/jest definitions file, the spyOn function is defined as the following:

function spyOn<T extends {}, M extends FunctionPropertyNames<Required<T>>>(
        object: T,
        method: M
    ): Required<T>[M] extends (...args: any[]) => any
        ? SpyInstance<ReturnType<Required<T>[M]>, ArgsType<Required<T>[M]>>
        : never;

I am having a lot of difficulty getting the TypeScript compiler to correctly read the below snippet without issue.

jest.spyOn<HttpResponse<FeatureFlag>, 'get'>(HttpClient, 'get').mockImplementationOnce(() => ({
      ok: true,
      data: {
        toggleActive: true,
      },
    }))

With the above setup, the compiler tells me that get does not satisfy the constraint never and I am at a loss of how to resolve the typing problem here to get my test written/running correctly. The other thing I'm trying to do here is only return a Partial of HttpResponse because I obviously don't want to have to fill in every field within my unit test when they are not needed.

Has anyone been able to apply jest.spyOn correctly with generics to help with this issue?

2 Answers

I think all you're missing is the word async in your call to mockImplementationOnce(). Then you can remove the type params for jest.spyOn. You don't get type safety within your mock implementation automatically, but if you're really worried about it, you can add as HttpResponse<FeatureFlag>.

let result: FetchResponse<boolean, string>;

describe('getFeatureFlag', () => {
    beforeEach(async () => {
        //                                                  missing
        //                                                     ↓
        jest.spyOn(HttpClient, 'get').mockImplementationOnce(async () => ({
            ok: true,
            data: {
                toggleActive: true,
            },
        } as HttpResponse<FeatureFlag>));

        result = await getFeatureFlag(flagName);
    })
});

TS Playground

Granted, this doesn't answer the question asked in the title, but it should fix your code. I messed around for a long time trying to get your specified generic type arguments to work, and eventually gave up. It may be a limitation of the declarations in the jest namespace.

Could you try to change your code like this:

jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(() => {... return <expectedResultObject>; })

This works for me with the typed-rest-client/RestClient npm package just like charm:

import * as rm from 'typed-rest-client/RestClient';
import {StatusCodes} from 'http-status-codes';

let apiSpy: jest.SpyInstance;

apiSpy = jest.spyOn(rm.RestClient.prototype, 'get');
apiSpy.mockImplementation(path => {
    const response: rm.IRestResponse<string> = {
        statusCode: StatusCodes.OK,
        headers: {},
        result: 'stringResult'
    }
    return response;
})
Related