Utility to automock class instance
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
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
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();
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);
}
}
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: @.***>
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
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).
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 !!
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
mockObjectutility 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
👍
We can expose something like this:
const SomeClass = vi.mockObject(OriginalClass)
const instance = new SomeClass() // Mock<OriginalClass>
expect(SomeClass).toHaveBeenCalled()
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);
});
Will #7761 provide this?
mockObject follows the same rules that module automocking does. It automocks everything.
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.mockObjectmocks 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()