Skip to content

Commit f4a682d

Browse files
authored
docs: document how to mock class and its methods (#6615)
1 parent 8a8d3f0 commit f4a682d

File tree

2 files changed

+168
-31
lines changed

2 files changed

+168
-31
lines changed

Diff for: docs/guide/mocking.md

+137-31
Original file line numberDiff line numberDiff line change
@@ -537,21 +537,135 @@ describe('delayed execution', () => {
537537
})
538538
```
539539

540-
## Cheat Sheet
540+
## Classes
541541

542-
:::info
543-
`vi` in the examples below is imported directly from `vitest`. You can also use it globally, if you set `globals` to `true` in your [config](/config/).
542+
You can mock an entire class with a single `vi.fn` call - since all classes are also functions, this works out of the box. Beware that currently Vitest doesn't respect the `new` keyword so the `new.target` is always `undefined` in the body of a function.
543+
544+
```ts
545+
class Dog {
546+
name: string
547+
548+
constructor(name: string) {
549+
this.name = name
550+
}
551+
552+
static getType(): string {
553+
return 'animal'
554+
}
555+
556+
speak(): string {
557+
return 'bark!'
558+
}
559+
560+
isHungry() {}
561+
feed() {}
562+
}
563+
```
564+
565+
We can re-create this class with ES5 functions:
566+
567+
```ts
568+
const Dog = vi.fn(function (name) {
569+
this.name = name
570+
})
571+
572+
// notice that static methods are mocked directly on the function,
573+
// not on the instance of the class
574+
Dog.getType = vi.fn(() => 'mocked animal')
575+
576+
// mock the "speak" and "feed" methods on every instance of a class
577+
// all `new Dog()` instances will inherit these spies
578+
Dog.prototype.speak = vi.fn(() => 'loud bark!')
579+
Dog.prototype.feed = vi.fn()
580+
```
581+
582+
::: tip WHEN TO USE?
583+
Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module:
584+
585+
```ts
586+
import { Dog } from './dog.js'
587+
588+
vi.mock(import('./dog.js'), () => {
589+
const Dog = vi.fn()
590+
Dog.prototype.feed = vi.fn()
591+
// ... other mocks
592+
return { Dog }
593+
})
594+
```
595+
596+
This method can also be used to pass an instance of a class to a function that accepts the same interface:
597+
598+
```ts
599+
// ./src/feed.ts
600+
function feed(dog: Dog) {
601+
// ...
602+
}
603+
604+
// ./tests/dog.test.ts
605+
import { expect, test, vi } from 'vitest'
606+
import { feed } from '../src/feed.js'
607+
608+
const Dog = vi.fn()
609+
Dog.prototype.feed = vi.fn()
610+
611+
test('can feed dogs', () => {
612+
const dogMax = new Dog('Max')
613+
614+
feed(dogMax)
615+
616+
expect(dogMax.feed).toHaveBeenCalled()
617+
expect(dogMax.isHungry()).toBe(false)
618+
})
619+
```
544620
:::
545621

546-
I want to…
622+
Now, when we create a new instance of the `Dog` class its `speak` method (alongside `feed`) is already mocked:
547623

548-
### Spy on a `method`
624+
```ts
625+
const dog = new Dog('Cooper')
626+
dog.speak() // loud bark!
627+
628+
// you can use built-in assertions to check the validity of the call
629+
expect(dog.speak).toHaveBeenCalled()
630+
```
631+
632+
We can reassign the return value for a specific instance:
633+
634+
```ts
635+
const dog = new Dog('Cooper')
636+
637+
// "vi.mocked" is a type helper, since
638+
// TypeScript doesn't know that Dog is a mocked class,
639+
// it wraps any function in a MockInstance<T> type
640+
// without validating if the function is a mock
641+
vi.mocked(dog.speak).mockReturnValue('woof woof')
642+
643+
dog.speak() // woof woof
644+
```
645+
646+
To mock the property, we can use the `vi.spyOn(dog, 'name', 'get')` method. This makes it possible to use spy assertions on the mocked property:
549647

550648
```ts
551-
const instance = new SomeClass()
552-
vi.spyOn(instance, 'method')
649+
const dog = new Dog('Cooper')
650+
651+
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max')
652+
653+
expect(dog.name).toBe('Max')
654+
expect(nameSpy).toHaveBeenCalledTimes(1)
553655
```
554656

657+
::: tip
658+
You can also spy on getters and setters using the same method.
659+
:::
660+
661+
## Cheat Sheet
662+
663+
:::info
664+
`vi` in the examples below is imported directly from `vitest`. You can also use it globally, if you set `globals` to `true` in your [config](/config/).
665+
:::
666+
667+
I want to…
668+
555669
### Mock exported variables
556670
```js
557671
// some-path.js
@@ -595,41 +709,29 @@ vi.spyOn(exports, 'method').mockImplementation(() => {})
595709

596710
1. Example with `vi.mock` and `.prototype`:
597711
```ts
598-
// some-path.ts
712+
// ./some-path.ts
599713
export class SomeClass {}
600714
```
601715
```ts
602716
import { SomeClass } from './some-path.js'
603717

604-
vi.mock('./some-path.js', () => {
718+
vi.mock(import('./some-path.js'), () => {
605719
const SomeClass = vi.fn()
606720
SomeClass.prototype.someMethod = vi.fn()
607721
return { SomeClass }
608722
})
609723
// SomeClass.mock.instances will have SomeClass
610724
```
611725

612-
2. Example with `vi.mock` and a return value:
613-
```ts
614-
import { SomeClass } from './some-path.js'
615-
616-
vi.mock('./some-path.js', () => {
617-
const SomeClass = vi.fn(() => ({
618-
someMethod: vi.fn()
619-
}))
620-
return { SomeClass }
621-
})
622-
// SomeClass.mock.returns will have returned object
623-
```
624-
625-
3. Example with `vi.spyOn`:
726+
2. Example with `vi.spyOn`:
626727

627728
```ts
628-
import * as exports from './some-path.js'
729+
import * as mod from './some-path.js'
629730

630-
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
631-
// whatever suites you from first two examples
632-
})
731+
const SomeClass = vi.fn()
732+
SomeClass.prototype.someMethod = vi.fn()
733+
734+
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass)
633735
```
634736

635737
### Spy on an object returned from a function
@@ -655,7 +757,7 @@ obj.method()
655757
// useObject.test.js
656758
import { useObject } from './some-path.js'
657759

658-
vi.mock('./some-path.js', () => {
760+
vi.mock(import('./some-path.js'), () => {
659761
let _cache
660762
const useObject = () => {
661763
if (!_cache) {
@@ -680,8 +782,8 @@ expect(obj.method).toHaveBeenCalled()
680782
```ts
681783
import { mocked, original } from './some-path.js'
682784

683-
vi.mock('./some-path.js', async (importOriginal) => {
684-
const mod = await importOriginal<typeof import('./some-path.js')>()
785+
vi.mock(import('./some-path.js'), async (importOriginal) => {
786+
const mod = await importOriginal()
685787
return {
686788
...mod,
687789
mocked: vi.fn()
@@ -691,6 +793,10 @@ original() // has original behaviour
691793
mocked() // is a spy function
692794
```
693795

796+
::: warning
797+
Don't forget that this only [mocks _external_ access](#mocking-pitfalls). In this example, if `original` calls `mocked` internally, it will always call the function defined in the module, not in the mock factory.
798+
:::
799+
694800
### Mock the current date
695801

696802
To mock `Date`'s time, you can use `vi.setSystemTime` helper function. This value will **not** automatically reset between different tests.
@@ -762,6 +868,6 @@ it('the value is restored before running an other test', () => {
762868
export default defineConfig({
763869
test: {
764870
unstubEnvs: true,
765-
}
871+
},
766872
})
767873
```

Diff for: test/core/test/jest-mock.test.ts

+31
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,35 @@ describe('jest mock compat layer', () => {
408408
testFn.mockRestore()
409409
expect(testFn()).toBe(true)
410410
})
411+
412+
abstract class Dog_ {
413+
public name: string
414+
415+
constructor(name: string) {
416+
this.name = name
417+
}
418+
419+
abstract speak(): string
420+
abstract feed(): void
421+
}
422+
423+
it('mocks classes', () => {
424+
const Dog = vi.fn<(name: string) => Dog_>(function Dog_(name: string) {
425+
this.name = name
426+
} as (this: any, name: string) => Dog_)
427+
428+
;(Dog as any).getType = vi.fn(() => 'mocked animal')
429+
430+
Dog.prototype.speak = vi.fn(() => 'loud bark!')
431+
Dog.prototype.feed = vi.fn()
432+
433+
const dogMax = new Dog('Max')
434+
expect(dogMax.name).toBe('Max')
435+
436+
expect(dogMax.speak()).toBe('loud bark!')
437+
expect(dogMax.speak).toHaveBeenCalled()
438+
439+
vi.mocked(dogMax.speak).mockReturnValue('woof woof')
440+
expect(dogMax.speak()).toBe('woof woof')
441+
})
411442
})

0 commit comments

Comments
 (0)