Búsqueda de sitios web

jestjs - TypeScript+Jest - ¿cómo usar jest.spyOn con métodos genéricos de TypeScript?


Tengo el siguiente objeto de TypeScript definido:

// 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

También tengo otro archivo, getFeatureFlag.ts, que utiliza este HttpClient en su implementación:

// 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 ...
}

Estoy en el proceso de escribir pruebas unitarias (usando Jest) para este archivo bajo getFeatureFlag.test.ts y tengo el siguiente fragmento dentro de él:

// 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/')
  })
})

Desde dentro del archivo de definiciones @types/jest, la función spyOn está definida de la siguiente manera:

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;

Estoy teniendo mucha dificultad para que el compilador de TypeScript lea correctamente el siguiente fragmento sin problemas.

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

Con la configuración anterior, el compilador me indica que get no cumple con la restricción never y no sé cómo resolver el problema de tipado aquí para poder escribir/ejecutar correctamente mi prueba. Lo otro que estoy tratando de hacer aquí es devolver únicamente un Partial de HttpResponse porque obviamente no quiero tener que completar cada campo dentro de mi prueba unitaria cuando no son necesarios.

¿Alguien ha podido aplicar jest.spyOn correctamente con genéricos para ayudar con este problema?

Respuesta #1

Creo que todo lo que te falta es la palabra async en tu llamada a mockImplementationOnce(). Luego puedes eliminar los parámetros de tipo para jest.spyOn. No obtienes seguridad de tipos dentro de tu implementación simulada automáticamente, pero si realmente te preocupa, puedes agregar 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);
    })
});

Campo de Juegos TS

Cierto, esto no responde a la pregunta planteada en el título, pero debería arreglar tu código. Jugueteé durante mucho tiempo tratando de hacer que funcionaran los argumentos de tipo genérico que especificaste, y eventualmente me rendí. Puede ser una limitación de las declaraciones en el espacio de nombres jest.

Respuesta #2

¿Podrías intentar cambiar tu código así:

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

Esto funciona para mí con el paquete npm typed-rest-client/RestClient como un encanto:

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;
})

Artículos relacionados