Mocking Classes
You can mock an entire class with a single vi.fn
call.
class Dog {
name: string
constructor(name: string) {
this.name = name
}
static getType(): string {
return 'animal'
}
greet = (): string => {
return `Hi! My name is ${this.name}!`
}
speak(): string {
return 'bark!'
}
isHungry() {}
feed() {}
}
We can re-create this class with vi.fn
(or vi.spyOn().mockImplementation()
):
const Dog = vi.fn(class {
static getType = vi.fn(() => 'mocked animal')
constructor(name) {
this.name = name
}
greet = vi.fn(() => `Hi! My name is ${this.name}!`)
speak = vi.fn(() => 'loud bark!')
feed = vi.fn()
})
WARNING
If a non-primitive is returned from the constructor function, that value will become the result of the new expression. In this case the [[Prototype]]
may not be correctly bound:
const CorrectDogClass = vi.fn(function (name) {
this.name = name
})
const IncorrectDogClass = vi.fn(name => ({
name
}))
const Marti = new CorrectDogClass('Marti')
const Newt = new IncorrectDogClass('Newt')
Marti instanceof CorrectDogClass // ✅ true
Newt instanceof IncorrectDogClass // ❌ false!
If you are mocking classes, prefer the class syntax over the function.
WHEN TO USE?
Generally speaking, you would re-create a class like this inside the module factory if the class is re-exported from another module:
import { Dog } from './dog.js'
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn(class {
feed = vi.fn()
// ... other mocks
})
return { Dog }
})
This method can also be used to pass an instance of a class to a function that accepts the same interface:
function feed(dog: Dog) {
// ...
}
import { expect, test, vi } from 'vitest'
import { feed } from '../src/feed.js'
const Dog = vi.fn(class {
feed = vi.fn()
})
test('can feed dogs', () => {
const dogMax = new Dog('Max')
feed(dogMax)
expect(dogMax.feed).toHaveBeenCalled()
expect(dogMax.isHungry()).toBe(false)
})
Now, when we create a new instance of the Dog
class its speak
method (alongside feed
and greet
) is already mocked:
const Cooper = new Dog('Cooper')
Cooper.speak() // loud bark!
Cooper.greet() // Hi! My name is Cooper!
// you can use built-in assertions to check the validity of the call
expect(Cooper.speak).toHaveBeenCalled()
expect(Cooper.greet).toHaveBeenCalled()
const Max = new Dog('Max')
// methods are not shared between instances if you assigned them directly
expect(Max.speak).not.toHaveBeenCalled()
expect(Max.greet).not.toHaveBeenCalled()
We can reassign the return value for a specific instance:
const dog = new Dog('Cooper')
// "vi.mocked" is a type helper, since
// TypeScript doesn't know that Dog is a mocked class,
// it wraps any function in a Mock<T> type
// without validating if the function is a mock
vi.mocked(dog.speak).mockReturnValue('woof woof')
dog.speak() // woof woof
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:
const dog = new Dog('Cooper')
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max')
expect(dog.name).toBe('Max')
expect(nameSpy).toHaveBeenCalledTimes(1)
TIP
You can also spy on getters and setters using the same method.
DANGER
Using classes with vi.fn()
was introduced in Vitest 4. Previously, you had to use function
and prototype
inheritence directly. See v3 guide.