marchaos / jest-mock-extended

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

How to handle hoisted mocks #87

Closed moltenice closed 1 year ago

moltenice commented 2 years ago

Let's take the following code snippet:

import { mockDeep } from "jest-mock-extended";
// ... other imports

jest.mock("./someObject", () => ({
  someObject: mockDeep<InstanceType<SomeClass>>(),
}));

This would fail because jest hoists jest.mock above all the imports and so mockDeep would be undefined.

How do I solve this problem? And would it be worthwhile adding documentation on how to do this?

phil-lgr commented 2 years ago
import { PrismaClient } from '@prisma/client';
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended';

const mockDeepFn = mockDeep; // set the import to a const that starts with 'mock' for Jest

import prisma from './prisma';

//
// Prisma setup for unit testing with Jest
//
// @see https://www.prisma.io/docs/guides/testing/unit-testing
//

jest.mock('./prisma', () => {
  var mockPrismaClient = mockDeepFn<PrismaClient>();
  return {
    __esModule: true,
    default: mockPrismaClient,
  };
});

beforeEach(() => {
  console.log(prismaMock);
  mockReset(prismaMock);
});

export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>;

I have a different error now, my prismaMock is not defined, but it fixed the hoisting problem

mckernanin commented 2 years ago

@phil-lgr did you get past the issue? I'm at the same point that you are.

toshi38 commented 1 year ago

Probably worth adding to docs... but you can do this:

import { mockDeep } from "jest-mock-extended";
// ... other imports

jest.mock("./someObject", () => {
    const nonHoistedMockDeep: typeof mockDeep = jest.requireActual("jest-mock-extended").mockDeep;
    return {
      someObject: nonHoistedMockDeep<SomeClass>(),
    };
});
ctsstc commented 1 year ago

I was wondering you'd have to do a dynamic import in the jest.mock or if that's even possible, but I forgot all about jest.requireActual.

moltenice commented 1 year ago

@toshi38 this is absolutely worth adding to the docs, this is a scenario that we would encounter quite frequently.

Closing this issue as resolved, but yes definitely worth adding to the docs!

ctsstc commented 1 year ago

I'm back here again from a Google search 😆 I think another important part of this that I'm not sure has been handled or explained is how to get proper type safety when utilizing jest.mock?

A large reason I enjoy this library is the proper typing it gives after mocking something. When using jest.mock though, there doesn't seem to be a great way around this. Has anyone figure anything out for that?

Edit

It seems this may be the best route; create a reference and type it as such:

This is the scenario where Something is an object literal, so I had to use typeof in the casting, but otherwise I imagine this applies to other classes as well [but w/o the typeof].

Dependency to be mocked

something.ts

export const Something = {
  someMethod: (data: SomeType): YetAnotherType => {
    // do stuff
    return new YetAnotherType(data)
  }
}

Consumer & Test

consumer.ts

import { Something } from 'something'

export class Consumer {
  doSomething() {
    return Something.someMethod(new SomeType())
  }
}

consumer.test.ts

jest.mock('./something', () => {
  const nonHoistedMockDeep: typeof mockDeep =
    jest.requireActual('jest-mock-extended').mockDeep

  return {
    Something: nonHoistedMockDeep<typeof Something>(),
  }
})

// This will give you proper type safe autocomplete & TS validation
const SomethingMock = <DeepMockProxy<typeof Something>>Something

beforeEach(() => {
  SomethingMock.someMethod.mockReturnValue()
})

It almost makes me want to go back to dependency injecting everything which jest-mock-extended works great for, but I feel like the language provides import as a feature that shouldn't be avoided or fought. It just feels like a lot of footwork for something that should be trivial.