vitest icon indicating copy to clipboard operation
vitest copied to clipboard

Utility to automock class instance

Open everett1992 opened this issue 2 years ago • 7 comments

Clear and concise description of the problem

As a developer using vitest I want an easy way to create mock instances of a class. While I can us vi.mock('module') to automock the entire file I have to jump thru hoops to get a mock instance, mainly to avoid TypeScript errors

import { Class } from 'module'
vi.mock('module')
// @ts-ignore-error Ignore constructor argument errors
const mockInstance  = new Class()

vi.mock will mock the whole module, so if you wanted another exported value to use the real implementation you need a factory to import the real implementation.

Suggested solution

import { Class } from 'module'
import { vi } from 'vitest'

const mock = vi.mockInstance(Class)

Alternative

No response

Additional context

No response

Validations

everett1992 avatar Aug 22 '23 15:08 everett1992

I would be very interested in this feature !!

In my particular case, I'm using Vitest to unit test Vue.js components. And I often need to instanciate a class to provide a mock to components that use inject. At the moment, I'm forced to use workarounds like the one suggested by @everett1992.

It would also be very useful to be able to retrieve the mocked methods of this mocked class instance, to specify a behaviour if needed, depending on the test case.

import { Class } from 'module'
vi.mock('module')
// @ts-ignore-error Ignore constructor argument errors
const mockInstance  = new Class()

mockInstance.someMethod.mockReturnValue('mocked value');
expect(mockInstance.someMethod).toHaveBeenCalled();

splanard avatar Apr 17 '24 16:04 splanard

Is this already possible with vi.importMock? This should return "automock"-ed module without actually setting up module mocking. I made a simple example here:

https://stackblitz.com/edit/vitest-dev-vitest-hnt4sl?file=src%2Fsome.test.ts

//
// some.test.ts
//
import { vi, test, expect } from 'vitest';

test('repro', async () => {
  const { SomeClass } = await vi.importMock<typeof import('./some-class')>('./some-class');

  const someInstance = new SomeClass();

  // type check works
  vi.mocked(someInstance.someMethod).mockReturnValue('hello');
  expect(someInstance.someMethod(1234)).toMatchInlineSnapshot(`"hello"`);
});


//
// some-class.ts
//
export class SomeClass {
  someMethod(x: number) {
    return String(x);
  }
}

hi-ogawa avatar Apr 17 '24 23:04 hi-ogawa

Add a constructor with parameters to SomeClass. You'll get a type error unless you provide it when calling the mocked constructor.

That's not too bad, you can add a ts-ignore or cast to suppress it. But that isn't elegant. We could use a simple helper similar to vi.mocked.

const instance = vitest.constructMocked(SomeClass)

constructMocked would call new on the argument.

On Wed, Apr 17, 2024, 4:53 PM Hiroshi Ogawa @.***> wrote:

Is this already possible with vi.importMock? This should return "automock"-ed module without actually setting up module mocking. I made a simple example here:

https://stackblitz.com/edit/vitest-dev-vitest-hnt4sl?file=src%2Fsome.test.ts

//// some.test.ts//import { vi, test, expect } from 'vitest'; test('repro', async () => { const { SomeClass } = await vi.importMock<typeof import('./some-class')>('./some-class');

const someInstance = new SomeClass();

// type check works vi.mocked(someInstance.someMethod).mockReturnValue('hello'); expect(someInstance.someMethod(1234)).toMatchInlineSnapshot("hello");});

//// some-class.ts//export class SomeClass { someMethod(x: number) { return String(x); }}

— Reply to this email directly, view it on GitHub https://github.com/vitest-dev/vitest/issues/4001#issuecomment-2062726086, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAE6WM6VOZMLQTB6EHLHAA3Y54DQZAVCNFSM6AAAAAA32DZXKSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANRSG4ZDMMBYGY . You are receiving this because you were mentioned.Message ID: @.***>

everett1992 avatar Apr 18 '24 00:04 everett1992

I see. Your concern was the type error on construction. Then I'm not sure if it's really important features to just help silencing type errors. Do you see more on this feature other than type errors?

EDIT: Well, I actually thought it would be a neat feature if we can expose mockObject utility for users to easily simulate automock without concerned with modules.

https://github.com/vitest-dev/vitest/blob/c84113f410d76e15fed3cebd83491f628396b5ab/packages/vitest/src/runtime/mocker.ts#L282

hi-ogawa avatar Apr 18 '24 00:04 hi-ogawa

Is this already possible with vi.importMock? This should return "automock"-ed module without actually setting up module mocking. I made a simple example here:

https://stackblitz.com/edit/vitest-dev-vitest-hnt4sl?file=src%2Fsome.test.ts

//
// some.test.ts
//
import { vi, test, expect } from 'vitest';

test('repro', async () => {
  const { SomeClass } = await vi.importMock<typeof import('./some-class')>('./some-class');

  const someInstance = new SomeClass();

  // type check works
  vi.mocked(someInstance.someMethod).mockReturnValue('hello');
  expect(someInstance.someMethod(1234)).toMatchInlineSnapshot(`"hello"`);
});


//
// some-class.ts
//
export class SomeClass {
  someMethod(x: number) {
    return String(x);
  }
}

I get this error while trying to instanciate the class your way :

This expression is not constructable.
  Type 'Promise<MockedObjectDeep<typeof import("...")>>' has no construct signatures.ts(2351)

The class is the only and default export of the module (single file class).

splanard avatar Apr 18 '24 12:04 splanard

Ok, it works this way :

//
// some.test.ts
//
import { vi, test, expect } from 'vitest';

test('repro', async () => {
  const SomeClass = (await vi.importMock<typeof import('./SomeClass')>('./SomeClass')).default;

  const someInstance = new SomeClass();

  // type check works
  vi.mocked(someInstance.someMethod).mockReturnValue('hello');
  expect(someInstance.someMethod(1234)).toMatchInlineSnapshot(`"hello"`);
});


//
// SomeClass.ts
//
export default class SomeClass {
  someMethod(x: number) {
    return String(x);
  }
}

Thx for the tip @hi-ogawa !!

splanard avatar Apr 18 '24 12:04 splanard

I see. Your concern was the type error on construction. Then I'm not sure if it's really important features to just help silencing type errors. Do you see more on this feature other than type errors?

EDIT: Well, I actually thought it would be a neat feature if we can expose mockObject utility for users to easily simulate automock without concerned with modules.

https://github.com/vitest-dev/vitest/blob/c84113f410d76e15fed3cebd83491f628396b5ab/packages/vitest/src/runtime/mocker.ts#L282

👍

superveetz-arb avatar Aug 21 '24 17:08 superveetz-arb

We can expose something like this:

const SomeClass = vi.mockObject(OriginalClass) 
const instance = new SomeClass() // Mock<OriginalClass>
expect(SomeClass).toHaveBeenCalled()

sheremet-va avatar Mar 28 '25 09:03 sheremet-va

We can expose something like this:

const SomeClass = vi.mockObject(OriginalClass) 
const instance = new SomeClass() // Mock<OriginalClass>
expect(SomeClass).toHaveBeenCalled()

I think at least as important is the ability to assert the mocked methods have been called; extending this example:

instance.someMethod("some argument")
expect(instance.someMethod).toHaveBeenCalledOnceWith("some argument");

Will #7761 provide this?

Also, I want to point out there is some daylight between the original proposal of

import { Class } from 'module'
import { vi } from 'vitest'

const mock = vi.mockInstance(Class)

and

const SomeClass = vi.mockObject(OriginalClass) 
const instance = new SomeClass() // Mock<OriginalClass>
expect(SomeClass).toHaveBeenCalled()

The original proposes giving users a way to create a mock object that behaves like an instance of a class. The example with vi.mockObject mocks the class, and the user seems to still have steps to go through to make a mock that behaves like an instance of that class.

I have code I'm currently trying to test that needs to receive instances of several classes:

// importer.ts

interface ImporterParams {
  serviceClient: ServiceClient;
  otherServiceClient: OtherServiceClient;
}

export class Importer {
  readonly serviceClient: ServiceClient;
  readonly otherServiceClient: OtherServiceClient;

  constructor(params: ImporterParams) {
    this.serviceClient = params.serviceClient;
    this.otherServiceClient = params.otherServiceClient;
  }

  async import(dataID: string) {
    const originalData = await this.serviceClient.getData(dataID);
    // ...
    await this.otherServiceClient.sendData(modifiedData);
    //...
  }
}

These are the tests I'd like to write for such code:

// importer.test.ts

beforeEach(() => {
  vi.mock(mockServiceClient.getData).mockResolvedValue(originalData);
  vi.mock(mockOtherServiceClient.sendData).mockResolvedValue({ "message": "Data imported." });
});

it("should send the dataID to service", async () => {
  await importer.importCases("data1234");
  expect(mockServiceClient.getData).toHaveBeenCalledWith("data1234");
});

it("should send the modified data to otherService", async () => {
  await importer.importCases("data1234");
  expect(mockOtherServiceClient.sendData).toHaveBeenCalledWith(expectedModifiedData);
});

gotgenes avatar Apr 30 '25 14:04 gotgenes

Will #7761 provide this?

mockObject follows the same rules that module automocking does. It automocks everything.

sheremet-va avatar Apr 30 '25 14:04 sheremet-va

I think at least as important is the ability to assert the mocked methods have been called; extending this example:

instance.someMethod("some argument") expect(instance.someMethod).toHaveBeenCalledOnceWith("some argument");

The original proposes giving users a way to create a mock object that behaves like an instance of a class. The example with vi.mockObject mocks the class, and the user seems to still have steps to go through to make a mock that behaves like an instance of that class.

The one originally suggested vi.mockInstance is only a special case. For example, if you already have an actual instance, you can do:

const instance = { ... }
const mockedInstance = vi.mockObject(instance);
mockedInstance.myFn();
expect(mockedInstance.myFn).toHaveBeenCalled()

and if you only have an actual class, you can do

const MockedClass = vi.mockObject(MyClass)
const mockedInstance = new MockedClass();  // or directly `new (vi.mockObject(MyClass))`
mockedInstance.myFn();
expect(mockedInstance.myFn).toHaveBeenCalled()

hi-ogawa avatar Apr 30 '25 23:04 hi-ogawa