9

I have a function which I would like to mock for testing purposes in TypeScript. In my tests, all I care about are the json and the status. However, when using Jest's jest.spyOn the type of my mocked function is set to return a http Response type. This is awkward as it means I have to manually go and implement a bunch of functions and properties that are irrelevant and arbitrary.

I suspect there is some way to use a partial type here to allow better and more useful mocking by overriding the return type to only that I care about. How would I go about doing this?

export function mockApi(json: object, status: number): void {
  jest.spyOn(
    myApiModule,
    'methodWhichReturnsAResponse'
  ).mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(json),
      status,
      // Below here is to appease jest types (not needed for
      // testing purposes at the time of writing)
      headers: {
        has: (name: string) => true,
        // get, set, etc...
      },
      ok: true,
      redirected: false,
      // and about 10 other properties which exist on the Response type
      // ...
    }),
  );
}

skyboyer
  • 22,209
  • 7
  • 57
  • 64
James Mulholland
  • 1,782
  • 1
  • 14
  • 21

4 Answers4

5

You can use as...

export function mockApi(json: object, status: number): void {
  jest.spyOn(
    myApiModule,
    'methodWhichReturnsAResponse'
  ).mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(json),
      status
    } as http.Response), // <-- here
  );
}

The as keyword, used for typecasting, when used to convert a literal to type X, will allow you to define it only partially, but you still have type-checking because you cannot define props that don't exist.

Example:

type X {
  a: number
  b: number
}

const x = { a: 2 } as X // OK
const y = { a: 3, c: 2 } as X // NOT OK, because c does not exist in X
Andre Pena
  • 56,650
  • 48
  • 196
  • 243
  • I was still getting an error when using `as` and was prompted to use an `unknown` type instead by the compiler. Perhaps I'm using a stricter `tsconfig.json` I'll post the solution I found from this below. Thank you for your help. – James Mulholland May 14 '19 at 13:11
  • That is really weird because `unknown` cannot be automatically converted to `http.Response` – Andre Pena May 14 '19 at 13:28
  • Should not be `as Promise` as the mock function is actually returning a promise? – Soullivaneuh May 11 '23 at 12:51
1

I found a solution using the unknown type.

After trying and failing to use as to typecast immediately, I first cast the promise to unknown and then cast this value to the desired Response type like so:

    // ...
    .mockImplementation(() => {
      const httpResponsePromise = Promise.resolve({
        json: () => Promise.resolve(json),
        status,
      }) as unknown;
      return httpResponsePromise as Promise<Response>;
    });
James Mulholland
  • 1,782
  • 1
  • 14
  • 21
1

I wrote the utility below which gives my codebase a partiallyMock<T>({}) call having property autocompletion for any type...

Demonstration of partiallyMock autocompletion in playground

Demonstration of mockWindow completion in playground

/** Simple mocking inspired by https://www.npmjs.com/package/jest-mock-extended
 * which has mockDeep<T>() for excellent autocompletion support but had other issues. */

/* atomic values (not made Partial when mocking) */
type Atomic = boolean | string | number | symbol | Date;

/** Mocks an indexed type (e.g. Object or Array), making it recursively Partial - note question mark  */
type PartialMockIndexed<T> = {
  [P in keyof T]?: PartialMock<T[P]>;
};

/** Mock any T */
export type PartialMock<T> = T extends Atomic ? T : PartialMockIndexed<T>;

/** Utility method for autocompleting a PartialMock<T> and returning it as a T */
export function partiallyMock<T>(mock: PartialMock<T>) {
  return mock as T;
}

/** Window is a special object, needs special mocking */
export function mockWindow(windowMock: PartialMock<typeof window>) {
  const origWindow = window;
  globalThis.window = Object.create(window);
  for (const [key, value] of Object.entries(windowMock)) {
    Object.defineProperty(globalThis.window, key, { value });
  }
  const unmockWindow = (globalThis.window = origWindow);
  return unmockWindow;
}
cefn
  • 2,895
  • 19
  • 28
0

Below is a TypeScript solution that is type-safe and allows partial return types for jest.mocked(...).

type GenericFunction = (...args: unknown[]) => unknown;
type GenericAsyncFunction = (...args: unknown[]) => Promise<unknown>;
type AwaitedReturnType<T extends GenericAsyncFunction> = Awaited<ReturnType<T>>;

type MockedFunc<T extends GenericFunction> = () => Partial<ReturnType<T>>;
type MockedFuncAsync<T extends GenericAsyncFunction> = () => Promise<Partial<AwaitedReturnType<T>>>;

export const partialMocked = <T extends MockedFunc<T>>(source: T) => jest.mocked<MockedFunc<T>>(source);
export const partialMockedAsync = <T extends MockedFuncAsync<T>>(source: T) => jest.mocked<MockedFuncAsync<T>>(source);

hanneswidrig
  • 185
  • 2
  • 13