При тестировании приложений на базе Vite часто возникает необходимость изолировать код от внешних зависимостей:
Для этого используется мокирование — подмена настоящей реализации объекта тестовой.
В экосистеме Vite основным инструментом тестирования обычно выступает Vitest, который предоставляет API, совместимый с Jest-подобным стилем тестирования.
Мокирование в Vitest строится вокруг объекта vi.
Основной способ подмены модуля:
vi.mock('./api.js', () => {
return {
getUsers: vi.fn()
}
})
Исходный модуль:
// api.js
export async function getUsers() {
const response = await fetch('/api/users')
return response.json()
}
Тест:
import { describe, it, expect, vi } from 'vitest'
import { getUsers } from './api.js'
vi.mock('./api.js', () => {
return {
getUsers: vi.fn(() => {
return Promise.resolve([
{ id: 1, name: 'Alex' }
])
})
}
})
describe('users', () => {
it('returns mocked users', async () => {
const users = await getUsers()
expect(users).toHaveLength(1)
})
})
После мокирования настоящий модуль больше не используется.
vi.mock() поднимается вверх файла аналогично
jest.mock().
Это означает:
const token = '123'
vi.mock('./config.js', () => {
return {
token
}
})
может работать не так, как ожидается, потому что мок выполняется раньше объявления некоторых переменных.
Правильный подход:
vi.mock('./config.js', () => {
return {
token: '123'
}
})
или:
const mockedToken = '123'
vi.mock('./config.js', () => {
return {
token: mockedToken
}
})
Иногда требуется заменить только часть функций.
vi.mock('./math.js', async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
sum: vi.fn(() => 100)
}
})
Исходный модуль:
export function sum(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
Тест:
import { sum, multiply } from './math.js'
console.log(sum(1, 2))
console.log(multiply(2, 3))
Результат:
100
6
Подменяется только sum.
Простейший mock function:
const fn = vi.fn()
Функция начинает собирать статистику вызовов:
fn('hello')
fn('world')
expect(fn).toHaveBeenCalledTimes(2)
const fn = vi.fn(() => 123)
или:
const fn = vi.fn()
fn.mockReturnValue(123)
const fn = vi.fn()
fn.mockResolvedValue({
success: true
})
Эквивалент:
const fn = vi.fn(() => Promise.resolve({
success: true
}))
const fn = vi.fn()
fn.mockRejectedValue(
new Error('API error')
)
Позволяет полностью заменить поведение:
const random = vi.fn()
random.mockImplementation(() => {
return Math.random() * 10
})
const fn = vi.fn()
fn
.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValue(3)
console.log(fn())
console.log(fn())
console.log(fn())
console.log(fn())
Результат:
1
2
3
3
expect(fn).toHaveBeenCalledWith('admin')
expect(fn).toHaveBeenCalledTimes(5)
expect(fn).toHaveBeenLastCalledWith('final')
Удаляет историю вызовов:
fn.mockClear()
Удаляет:
fn.mockReset()
Возвращает оригинальную реализацию.
Применяется только для spy.
spy.mockRestore()
spyOn не заменяет модуль полностью.
Он отслеживает существующую функцию.
import * as math from './math.js'
const spy = vi.spyOn(math, 'sum')
math.sum(1, 2)
expect(spy).toHaveBeenCalled()
const spy = vi.spyOn(math, 'sum')
spy.mockImplementation(() => 999)
global.fetch = vi.fn(() => {
return Promise.resolve({
json: () => Promise.resolve({
users: []
})
})
})
const fetchMock = vi
.spyOn(global, 'fetch')
.mockResolvedValue({
json: async () => ({
users: []
})
})
После теста:
fetchMock.mockRestore()
import axios from 'axios'
vi.mock('axios')
axios.get.mockResolvedValue({
data: [
{ id: 1 }
]
})
vi.useFakeTimers()
vi.setSystemTime(
new Date('2025-01-01')
)
Теперь:
new Date()
всегда возвращает одну и ту же дату.
vi.useRealTimers()
vi.useFakeTimers()
const callback = vi.fn()
setTimeout(callback, 1000)
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalled()
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn()
}
vi.stubGlobal(
'localStorage',
localStorageMock
)
Используется для подмены глобальных объектов.
vi.stubGlobal('MY_GLOBAL', {
enabled: true
})
Vite предоставляет переменные окружения через:
import.meta.env
Например:
VITE_API_URL=https://api.site.com
Использование:
const api = import.meta.env.VITE_API_URL
Во время тестов переменные окружения могут отсутствовать.
Например:
console.log(import.meta.env.VITE_API_URL)
вернёт:
undefined
import.meta.env.VITE_API_URL =
'http://localhost:3000'
Vitest предоставляет специальный API:
vi.stubEnv(
'VITE_API_URL',
'http://localhost:3000'
)
Проверка:
expect(
import.meta.env.VITE_API_URL
).toBe('http://localhost:3000')
vi.unstubAllEnvs()
Обычно используется:
afterEach(() => {
vi.unstubAllEnvs()
})
В Node.js-проектах иногда используется:
process.env.API_KEY
Тест:
process.env.API_KEY = 'secret'
или:
vi.stubEnv('API_KEY', 'secret')
Vitest умеет работать и с process.env, и с
import.meta.env.
| Особенность | process.env | import.meta.env |
|---|---|---|
| Node.js | Да | Нет |
| Браузер | Нет | Да |
| Vite | Частично | Да |
| Префикс VITE_ | Нет | Да |
Vite не экспортирует произвольные env-переменные в клиентский код.
Работают только:
VITE_API_URL=...
Не работают:
API_SECRET=...
Это механизм безопасности.
vi.mock('dotenv', () => {
return {
config: vi.fn()
}
})
Некоторые плагины создают виртуальные модули:
virtual:config
Мокирование:
vi.mock('virtual:config', () => {
return {
apiUrl: 'http://localhost'
}
})
Vite поддерживает импорт CSS:
import styles from './Button.module.css'
В тестах классы можно подменить:
vi.mock('./Button.module.css', () => {
return {
default: {
button: 'button'
}
}
})
vi.mock('./logo.svg', () => {
return {
default: '/mocked/logo.svg'
}
})
Vitest поддерживает auto-mocking:
vi.mock('./service.js')
Без фабрики.
Тогда функции автоматически превращаются в mock-функции.
Vitest хранит историю вызовов.
const fn = vi.fn()
fn('a')
fn('b')
Данные:
console.log(fn.mock.calls)
Результат:
[
['a'],
['b']
]
console.log(fn.mock.results)
vi.mock('./User.js', () => {
return {
User: vi.fn().mockImplementation(() => {
return {
save: vi.fn()
}
})
}
})
export const store = {
users: []
}
Состояние сохраняется между тестами.
beforeEach(() => {
vi.resetModules()
})
Сбрасывает кэш импортов:
vi.resetModules()
После этого модуль импортируется заново.
Позволяет выполнить импорт в изолированном контексте.
vi.isolateModules(async () => {
const module = await import('./store.js')
console.log(module)
})
vi.mock('./useAuth.js', () => {
return {
useAuth: () => ({
isAuth: true
})
}
})
vi.mock('./useTheme.js', () => {
return {
useTheme: () => ({
theme: 'dark'
})
}
})
Vitest поддерживает глобальные setup-файлы.
Конфигурация:
export default defineConfig({
test: {
setupFiles: [
'./tests/setup.js'
]
}
})
import { vi } from 'vitest'
vi.stubEnv(
'VITE_API_URL',
'http://localhost:3000'
)
vi.stubGlobal(
'fetch',
vi.fn()
)
Автоматическое восстановление моков:
export default defineConfig({
test: {
restoreMocks: true
}
})
Автоматическая очистка истории вызовов:
export default defineConfig({
test: {
clearMocks: true
}
})
Полный сброс mock-функций:
export default defineConfig({
test: {
mockReset: true
}
})
| Опция | История вызовов | Реализация | Оригинальная функция |
|---|---|---|---|
| clearMocks | Сбрасывается | Сохраняется | Нет |
| mockReset | Сбрасывается | Удаляется | Нет |
| restoreMocks | Сбрасывается | Оригинал | Да |
Чрезмерное количество моков приводит к:
Плохой пример:
vi.mock('./db.js')
vi.mock('./api.js')
vi.mock('./cache.js')
vi.mock('./logger.js')
vi.mock('./auth.js')
Тест почти ничего не проверяет.
Моки оправданы при работе с:
Практически полезная структура тестирования:
Такой подход обеспечивает: