Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/api/mock.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,47 @@ fn.length // == 2
The custom function implementation in the types below is marked with a generic `<T>`.
:::

::: warning Class Support {#class-support}
Shorthand methods like `mockReturnValue`, `mockReturnValueOnce`, `mockResolvedValue` and others cannot be used on a mocked class. Class constructors have [unintuitive behaviour](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) regarding the return value:

```ts {2,7}
const CorrectDogClass = vi.fn(class {
constructor(public name: string) {}
})

const IncorrectDogClass = vi.fn(class {
constructor(public name: string) {
return { name }
}
})

const Marti = new CorrectDogClass('Marti')
const Newt = new IncorrectDogClass('Newt')

Marti instanceof CorrectDogClass // ✅ true
Newt instanceof IncorrectDogClass // ❌ false!
```

Even though the shapes are the same, the _return value_ from the constructor is assigned to `Newt`, which is a plain object, not an instance of a mock. Vitest guards you against this behaviour in shorthand methods (but not in `mockImplementation`!) and throws an error instead.

If you need to mock constructed instance of a class, consider using the `class` syntax with `mockImplementation` instead:

```ts
mock.mockReturnValue({ hello: () => 'world' }) // [!code --]
mock.mockImplementation(class { hello = () => 'world' }) // [!code ++]
```

If you need to test the behaviour where this is a valid use case, you can use `mockImplementation` with a `constructor`:

```ts
mock.mockImplementation(class {
constructor(name: string) {
return { name }
}
})
```
:::

## getMockImplementation

```ts
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export default antfu(
`**/*.md/${GLOB_SRC}`,
],
rules: {
'prefer-arrow-callback': 'off',
'perfectionist/sort-imports': 'off',
'style/max-statements-per-line': 'off',
'import/newline-after-import': 'off',
Expand Down
54 changes: 48 additions & 6 deletions packages/spy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,63 @@ export function createMockInstance(options: MockInstanceOption = {}): Mock<Proce
}

mock.mockReturnValue = function mockReturnValue(value) {
return mock.mockImplementation(() => value)
return mock.mockImplementation(function () {
if (new.target) {
throwConstructorError('mockReturnValue')
}

return value
})
}

mock.mockReturnValueOnce = function mockReturnValueOnce(value) {
return mock.mockImplementationOnce(() => value)
return mock.mockImplementationOnce(function () {
if (new.target) {
throwConstructorError('mockReturnValueOnce')
}

return value
})
}

mock.mockResolvedValue = function mockResolvedValue(value) {
return mock.mockImplementation(() => Promise.resolve(value))
return mock.mockImplementation(function () {
if (new.target) {
throwConstructorError('mockResolvedValue')
}

return Promise.resolve(value)
})
}

mock.mockResolvedValueOnce = function mockResolvedValueOnce(value) {
return mock.mockImplementationOnce(() => Promise.resolve(value))
return mock.mockImplementationOnce(function () {
if (new.target) {
throwConstructorError('mockResolvedValueOnce')
}

return Promise.resolve(value)
})
}

mock.mockRejectedValue = function mockRejectedValue(value) {
return mock.mockImplementation(() => Promise.reject(value))
return mock.mockImplementation(function () {
if (new.target) {
throwConstructorError('mockRejectedValue')
}

return Promise.reject(value)
})
}

mock.mockRejectedValueOnce = function mockRejectedValueOnce(value) {
return mock.mockImplementationOnce(() => Promise.reject(value))
return mock.mockImplementationOnce(function () {
if (new.target) {
throwConstructorError('mockRejectedValueOnce')
}

return Promise.reject(value)
})
}

mock.mockClear = function mockClear() {
Expand Down Expand Up @@ -644,6 +680,12 @@ export function resetAllMocks(): void {
REGISTERED_MOCKS.forEach(mock => mock.mockReset())
}

function throwConstructorError(shorthand: string): never {
throw new TypeError(
`Cannot use \`${shorthand}\` when called with \`new\`. Use \`mockImplementation\` with a \`class\` keyword instead. See https://vitest.dev/api/mock#class-support for more information.`,
)
}

export type {
Constructable,
MaybeMocked,
Expand Down
2 changes: 1 addition & 1 deletion packages/spy/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export type MockParameters<T extends Procedure | Constructable> = T extends Cons
? Parameters<T> : never

export type MockReturnType<T extends Procedure | Constructable> = T extends Constructable
? void
? InstanceType<T>
: T extends Procedure
? ReturnType<T> : never

Expand Down
4 changes: 2 additions & 2 deletions test/core/test/mocking/vi-fn.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ test('spy.mock when implementation is a class', () => {
const Mock = vi.fn(Klass)

expectTypeOf(Mock.mock.calls).toEqualTypeOf<[a: string, b?: number][]>()
expectTypeOf(Mock.mock.results).toEqualTypeOf<MockResult<void>[]>()
expectTypeOf(Mock.mock.results).toEqualTypeOf<MockResult<Klass>[]>()
expectTypeOf(Mock.mock.contexts).toEqualTypeOf<Klass[]>()
expectTypeOf(Mock.mock.instances).toEqualTypeOf<Klass[]>()
expectTypeOf(Mock.mock.invocationCallOrder).toEqualTypeOf<number[]>()
expectTypeOf(Mock.mock.settledResults).toEqualTypeOf<MockSettledResult<void>[]>()
expectTypeOf(Mock.mock.settledResults).toEqualTypeOf<MockSettledResult<Klass>[]>()
expectTypeOf(Mock.mock.lastCall).toEqualTypeOf<[a: string, b?: number] | undefined>()

// static properties are defined
Expand Down
48 changes: 48 additions & 0 deletions test/core/test/mocking/vi-fn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,54 @@ describe('vi.fn() implementations', () => {
expect(callArgs).toEqual(['test', 42])
expect(Mock.mock.calls).toEqual([['test', 42]])
})

test('vi.fn() with mockReturnValue throws when called with new', () => {
const Mock = vi.fn()
Mock.mockReturnValue(42)
expect(() => new Mock()).toThrowError(
'Cannot use `mockReturnValue` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})

test('vi.fn() with mockReturnValueOnce throws when called with new', () => {
const Mock = vi.fn()
Mock.mockReturnValueOnce(42)
expect(() => new Mock()).toThrowError(
'Cannot use `mockReturnValueOnce` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})

test('vi.fn() with mockResolvedValue throws when called with new', () => {
const Mock = vi.fn()
Mock.mockResolvedValue(42)
expect(() => new Mock()).toThrowError(
'Cannot use `mockResolvedValue` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})

test('vi.fn() with mockResolvedValueOnce throws when called with new', () => {
const Mock = vi.fn()
Mock.mockResolvedValueOnce(42)
expect(() => new Mock()).toThrowError(
'Cannot use `mockResolvedValueOnce` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})

test('vi.fn() with mockRejectedValue throws when called with new', () => {
const Mock = vi.fn()
Mock.mockRejectedValue(new Error('test'))
expect(() => new Mock()).toThrowError(
'Cannot use `mockRejectedValue` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})

test('vi.fn() with mockRejectedValueOnce throws when called with new', () => {
const Mock = vi.fn()
Mock.mockRejectedValueOnce(new Error('test'))
expect(() => new Mock()).toThrowError(
'Cannot use `mockRejectedValueOnce` when called with `new`. Use `mockImplementation` with a `class` keyword instead.',
)
})
})

function assertStateEmpty(state: MockContext<any>) {
Expand Down
Loading