Mocking
When writing tests it's only a matter of time before you need to create a "fake" version of an internal — or external — service. This is commonly referred to as mocking. Vitest provides utility functions to help you out through its vi helper. You can import { vi } from 'vitest'
or access it globally (when global configuration is enabled).
WARNING
Always remember to clear or restore mocks before or after each test run to undo mock state changes between runs! See mockReset
docs for more info.
If you wanna dive in head first, check out the API section otherwise keep reading to take a deeper dive into the world of mocking.
Dates
Sometimes you need to be in control of the date to ensure consistency when testing. Vitest uses @sinonjs/fake-timers
package for manipulating timers, as well as system date. You can find more about the specific API in detail here.
Example
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const businessHours = [9, 17]
function purchase() {
const currentHour = new Date().getHours()
const [open, close] = businessHours
if (currentHour > open && currentHour < close)
return { message: 'Success' }
return { message: 'Error' }
}
describe('purchasing flow', () => {
beforeEach(() => {
// tell vitest we use mocked time
vi.useFakeTimers()
})
afterEach(() => {
// restoring date after each test run
vi.useRealTimers()
})
it('allows purchases within business hours', () => {
// set hour within business hours
const date = new Date(2000, 1, 1, 13)
vi.setSystemTime(date)
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Success' })
})
it('disallows purchases outside of business hours', () => {
// set hour outside business hours
const date = new Date(2000, 1, 1, 19)
vi.setSystemTime(date)
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Error' })
})
})
Functions
Mocking functions can be split up into two different categories; spying & mocking.
Sometimes all you need is to validate whether or not a specific function has been called (and possibly which arguments were passed). In these cases a spy would be all we need which you can use directly with vi.spyOn()
(read more here).
However spies can only help you spy on functions, they are not able to alter the implementation of those functions. In the case where we do need to create a fake (or mocked) version of a function we can use vi.fn()
(read more here).
We use Tinyspy as a base for mocking functions, but we have our own wrapper to make it jest
compatible. Both vi.fn()
and vi.spyOn()
share the same methods, however only the return result of vi.fn()
is callable.
Example
import { afterEach, describe, expect, it, vi } from 'vitest'
function getLatest(index = messages.items.length - 1) {
return messages.items[index]
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // can also be a `getter or setter if supported`
}
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest')
expect(spy.getMockName()).toEqual('getLatest')
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1],
)
expect(spy).toHaveBeenCalledTimes(1)
spy.mockImplementationOnce(() => 'access-restricted')
expect(messages.getLatest()).toEqual('access-restricted')
expect(spy).toHaveBeenCalledTimes(2)
})
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(1)
mock.mockImplementationOnce(() => 'access-restricted')
expect(mock()).toEqual('access-restricted')
expect(mock).toHaveBeenCalledTimes(2)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(3)
})
})
More
Globals
You can mock global variables that are not present with jsdom
or node
by using vi.stubGlobal
helper. It will put the value of the global variable into a globalThis
object.
import { vi } from 'vitest'
const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}))
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock)
// now you can access it as `IntersectionObserver` or `window.IntersectionObserver`
Modules
Mock modules observe third-party-libraries, that are invoked in some other code, allowing you to test arguments, output or even redeclare its implementation.
See the vi.mock()
API section for a more in-depth detailed API description.
Automocking Algorithm
If your code is importing a mocked module, without any associated __mocks__
file or factory
for this module, Vitest will mock the module itself by invoking it and mocking every export.
The following principles apply
- All arrays will be emptied
- All primitives and collections will stay the same
- All objects will be deeply cloned
- All instances of classes and their prototypes will be deeply cloned
Virtual Modules
Vitest supports mocking Vite virtual modules. It works differently from how virtual modules are treated in Jest. Instead of passing down virtual: true
to a vi.mock
function, you need to tell Vite that module exists otherwise it will fail during parsing. You can do that in several ways:
- Provide an alias
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js')
}
}
}
- Provide a plugin that resolves a virtual module
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms')
return 'virtual:$app/forms'
}
}
]
}
The benefit of the second approach is that you can dynamically create different virtual entrypoints. If you redirect several virtual modules into a single file, then all of them will be affected by vi.mock
, so make sure to use unique identifiers.
Mocking Pitfalls
Beware that it is not possible to mock calls to methods that are called inside other methods of the same file. For example, in this code:
export function foo() {
return 'foo'
}
export function foobar() {
return `${foo()}bar`
}
It is not possible to mock the foo
method from the outside because it is referenced directly. So this code will have no effect on the foo
call inside foobar
(but it will affect the foo
call in other modules):
import { vi } from 'vitest'
import * as mod from './foobar.js'
// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
return {
...await importOriginal<typeof import('./foobar.js')>(),
// this will only affect "foo" outside of the original module
foo: () => 'mocked'
}
})
You can confirm this behaviour by providing the implementation to the foobar
method directly:
// foobar.test.js
import * as mod from './foobar.js'
vi.spyOn(mod, 'foo')
// exported foo references mocked method
mod.foobar(mod.foo)
// foobar.js
export function foo() {
return 'foo'
}
export function foobar(injectedFoo) {
return injectedFoo !== foo // false
}
This is the intended behaviour. It is usually a sign of bad code when mocking is involved in such a manner. Consider refactoring your code into multiple files or improving your application architecture by using techniques such as dependency injection.
Example
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Client } from 'pg'
import { failure, success } from './handlers.js'
// handlers
export function success(data) {}
export function failure(data) {}
// get todos
export async function getTodos(event, context) {
const client = new Client({
// ...clientOptions
})
await client.connect()
try {
const result = await client.query('SELECT * FROM todos;')
client.end()
return success({
message: `${result.rowCount} item(s) returned`,
data: result.rows,
status: true,
})
}
catch (e) {
console.error(e.stack)
client.end()
return failure({ message: e, status: false })
}
}
vi.mock('pg', () => {
const Client = vi.fn()
Client.prototype.connect = vi.fn()
Client.prototype.query = vi.fn()
Client.prototype.end = vi.fn()
return { Client }
})
vi.mock('./handlers.js', () => {
return {
success: vi.fn(),
failure: vi.fn(),
}
})
describe('get a list of todo items', () => {
let client
beforeEach(() => {
client = new Client()
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return items successfully', async () => {
client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 })
await getTodos()
expect(client.connect).toBeCalledTimes(1)
expect(client.query).toBeCalledWith('SELECT * FROM todos;')
expect(client.end).toBeCalledTimes(1)
expect(success).toBeCalledWith({
message: '0 item(s) returned',
data: [],
status: true,
})
})
it('should throw an error', async () => {
const mError = new Error('Unable to retrieve rows')
client.query.mockRejectedValueOnce(mError)
await getTodos()
expect(client.connect).toBeCalledTimes(1)
expect(client.query).toBeCalledWith('SELECT * FROM todos;')
expect(client.end).toBeCalledTimes(1)
expect(failure).toBeCalledWith({ message: mError, status: false })
})
})
Requests
Because Vitest runs in Node, mocking network requests is tricky; web APIs are not available, so we need something that will mimic network behavior for us. We recommend Mock Service Worker to accomplish this. It will let you mock both REST
and GraphQL
network requests, and is framework agnostic.
Mock Service Worker (MSW) works by intercepting the requests your tests make, allowing you to use it without changing any of your application code. In-browser, this uses the Service Worker API. In Node.js, and for Vitest, it uses the @mswjs/interceptors
library. To learn more about MSW, read their introduction
Configuration
You can use it like below in your setup file
import { afterAll, afterEach, beforeAll } from 'vitest'
import { setupServer } from 'msw/node'
import { HttpResponse, graphql, http } from 'msw'
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
]
export const restHandlers = [
http.get('https://rest-endpoint.example/path/to/posts', () => {
return HttpResponse.json(posts)
}),
]
const graphqlHandlers = [
graphql.query('ListPosts', () => {
return HttpResponse.json(
{
data: { posts },
},
)
}),
]
const server = setupServer(...restHandlers, ...graphqlHandlers)
// Start server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Close server after all tests
afterAll(() => server.close())
// Reset handlers after each test `important for test isolation`
afterEach(() => server.resetHandlers())
Configuring the server with
onUnhandleRequest: 'error'
ensures that an error is thrown whenever there is a request that does not have a corresponding request handler.
Example
We have a full working example which uses MSW: React Testing with MSW.
More
There is much more to MSW. You can access cookies and query parameters, define mock error responses, and much more! To see all you can do with MSW, read their documentation.
Timers
When we test code that involves timeouts or intervals, instead of having our tests wait it out or timeout, we can speed up our tests by using "fake" timers that mock calls to setTimeout
and setInterval
.
See the vi.useFakeTimers
API section for a more in depth detailed API description.
Example
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2) // 2 hours
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60) // 1 minute
}
const mock = vi.fn(() => console.log('executed'))
describe('delayed execution', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should execute the function', () => {
executeAfterTwoHours(mock)
vi.runAllTimers()
expect(mock).toHaveBeenCalledTimes(1)
})
it('should not execute the function', () => {
executeAfterTwoHours(mock)
// advancing by 2ms won't trigger the func
vi.advanceTimersByTime(2)
expect(mock).not.toHaveBeenCalled()
})
it('should execute every minute', () => {
executeEveryMinute(mock)
vi.advanceTimersToNextTimer()
expect(mock).toHaveBeenCalledTimes(1)
vi.advanceTimersToNextTimer()
expect(mock).toHaveBeenCalledTimes(2)
})
})
Cheat Sheet
INFO
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.
I want to…
- Spy on a
method
const instance = new SomeClass()
vi.spyOn(instance, 'method')
- Mock exported variables
// some-path.js
export const getter = 'variable'
// some-path.test.ts
import * as exports from './some-path.js'
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked')
- Mock exported function
Example with vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js'
vi.mock('./some-path.js', () => ({
method: vi.fn()
}))
WARNING
Don't forget that vi.mock
call is hoisted to top of the file. Do not put vi.mock
calls inside beforeEach
, only one of these will actually mock a module.
Example with vi.spyOn
:
import * as exports from './some-path.js'
vi.spyOn(exports, 'method').mockImplementation(() => {})
- Mock exported class implementation
Example with vi.mock
and prototype:
// some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js'
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn()
SomeClass.prototype.someMethod = vi.fn()
return { SomeClass }
})
// SomeClass.mock.instances will have SomeClass
Example with vi.mock
and return value:
import { SomeClass } from './some-path.js'
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn()
}))
return { SomeClass }
})
// SomeClass.mock.returns will have returned object
Example with vi.spyOn
:
import * as exports from './some-path.js'
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// whatever suites you from first two examples
})
- Spy on an object returned from a function
Example using cache:
// some-path.ts
export function useObject() {
return { method: () => true }
}
// useObject.js
import { useObject } from './some-path.js'
const obj = useObject()
obj.method()
// useObject.test.js
import { useObject } from './some-path.js'
vi.mock('./some-path.js', () => {
let _cache
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
}
}
// now every time that useObject() is called it will
// return the same object reference
return _cache
}
return { useObject }
})
const obj = useObject()
// obj.method was called inside some-path
expect(obj.method).toHaveBeenCalled()
- Mock part of a module
import { mocked, original } from './some-path.js'
vi.mock('./some-path.js', async (importOriginal) => {
const mod = await importOriginal<typeof import('./some-path.js')>()
return {
...mod,
mocked: vi.fn()
}
})
original() // has original behaviour
mocked() // is a spy function
- Mock current date
To mock Date
's time, you can use vi.setSystemTime
helper function. This value will not automatically reset between different tests.
Beware that using vi.useFakeTimers
also changes the Date
's time.
const mockDate = new Date(2022, 0, 1)
vi.setSystemTime(mockDate)
const now = new Date()
expect(now.valueOf()).toBe(mockDate.valueOf())
// reset mocked time
vi.useRealTimers()
- Mock global variable
You can set global variable by assigning a value to globalThis
or using vi.stubGlobal
helper. When using vi.stubGlobal
, it will not automatically reset between different tests, unless you enable unstubGlobals
config option or call vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')
- Mock
import.meta.env
To change environmental variable, you can just assign a new value to it. This value will not automatically reset between different tests.
import { beforeEach, expect, it } from 'vitest'
// you can reset it in beforeEach hook manually
const originalViteEnv = import.meta.env.VITE_ENV
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv
})
it('changes value', () => {
import.meta.env.VITE_ENV = 'staging'
expect(import.meta.env.VITE_ENV).toBe('staging')
})
If you want to automatically reset value, you can use vi.stubEnv
helper with unstubEnvs
config option enabled (or call vi.unstubAllEnvs
manually in beforeEach
hook):
import { expect, it, vi } from 'vitest'
// before running tests "VITE_ENV" is "test"
import.meta.env.VITE_ENV === 'test'
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging')
expect(import.meta.env.VITE_ENV).toBe('staging')
})
it('the value is restored before running an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test')
})
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
}
}