Мокирование модулей и переменных окружения

При тестировании приложений на базе Vite часто возникает необходимость изолировать код от внешних зависимостей:

  • HTTP-клиентов
  • API
  • модулей браузера
  • переменных окружения
  • конфигурационных файлов
  • таймеров
  • глобальных объектов

Для этого используется мокирование — подмена настоящей реализации объекта тестовой.

В экосистеме Vite основным инструментом тестирования обычно выступает Vitest, который предоставляет API, совместимый с Jest-подобным стилем тестирования.

Мокирование в Vitest строится вокруг объекта vi.


Мокирование модулей

Базовое мокирование через vi.mock

Основной способ подмены модуля:

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)
    })
})

После мокирования настоящий модуль больше не используется.


Особенности hoisting в vi.mock

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
    }
})

Частичное мокирование модуля

Иногда требуется заменить только часть функций.

Использование importOriginal

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.


Мокирование функций

vi.fn

Простейший 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')
)

mockImplementation

Позволяет полностью заменить поведение:

const random = vi.fn()

random.mockImplementation(() => {
    return Math.random() * 10
})

Последовательные ответы

mockReturnValueOnce

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')

Очистка моков

mockClear

Удаляет историю вызовов:

fn.mockClear()

mockReset

Удаляет:

  • историю вызовов
  • реализации
  • return values
fn.mockReset()

mockRestore

Возвращает оригинальную реализацию.

Применяется только для spy.

spy.mockRestore()

Мокирование через vi.spyOn

spyOn не заменяет модуль полностью.

Он отслеживает существующую функцию.

Пример

import * as math from './math.js'

const spy = vi.spyOn(math, 'sum')

math.sum(1, 2)

expect(spy).toHaveBeenCalled()

Подмена реализации spy

const spy = vi.spyOn(math, 'sum')

spy.mockImplementation(() => 999)

Мокирование fetch

Глобальный fetch

global.fetch = vi.fn(() => {
    return Promise.resolve({
        json: () => Promise.resolve({
            users: []
        })
    })
})

Более безопасный вариант

const fetchMock = vi
    .spyOn(global, 'fetch')
    .mockResolvedValue({
        json: async () => ({
            users: []
        })
    })

После теста:

fetchMock.mockRestore()

Мокирование axios

import axios from 'axios'

vi.mock('axios')

axios.get.mockResolvedValue({
    data: [
        { id: 1 }
    ]
})

Мокирование Date

Фиксация времени

vi.useFakeTimers()

vi.setSystemTime(
    new Date('2025-01-01')
)

Теперь:

new Date()

всегда возвращает одну и ту же дату.


Возврат настоящих таймеров

vi.useRealTimers()

Мокирование setTimeout

Управление таймерами

vi.useFakeTimers()

const callback = vi.fn()

setTimeout(callback, 1000)

vi.advanceTimersByTime(1000)

expect(callback).toHaveBeenCalled()

Мокирование localStorage

const localStorageMock = {
    getItem: vi.fn(),
    setItem: vi.fn(),
    removeItem: vi.fn()
}

vi.stubGlobal(
    'localStorage',
    localStorageMock
)

vi.stubGlobal

Используется для подмены глобальных объектов.

vi.stubGlobal('MY_GLOBAL', {
    enabled: true
})

Мокирование import.meta.env

Vite предоставляет переменные окружения через:

import.meta.env

Например:

VITE_API_URL=https://api.site.com

Использование:

const api = import.meta.env.VITE_API_URL

Проблема тестирования import.meta.env

Во время тестов переменные окружения могут отсутствовать.

Например:

console.log(import.meta.env.VITE_API_URL)

вернёт:

undefined

Способы мокирования import.meta.env

Прямое изменение

import.meta.env.VITE_API_URL =
    'http://localhost:3000'

Через vi.stubEnv

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()
})

Мокирование process.env

В 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

Особенность process.env import.meta.env
Node.js Да Нет
Браузер Нет Да
Vite Частично Да
Префикс VITE_ Нет Да

Почему нужен префикс VITE_

Vite не экспортирует произвольные env-переменные в клиентский код.

Работают только:

VITE_API_URL=...

Не работают:

API_SECRET=...

Это механизм безопасности.


Мокирование dotenv-конфигураций

Пример

vi.mock('dotenv', () => {
    return {
        config: vi.fn()
    }
})

Мокирование виртуальных модулей Vite

Некоторые плагины создают виртуальные модули:

virtual:config

Мокирование:

vi.mock('virtual:config', () => {
    return {
        apiUrl: 'http://localhost'
    }
})

Мокирование CSS-модулей

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-функции.


Проверка mock.calls

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()
            }
        })
    }
})

Мокирование singleton-модулей

Проблема singleton

export const store = {
    users: []
}

Состояние сохраняется между тестами.


Решение

beforeEach(() => {
    vi.resetModules()
})

resetModules

Сбрасывает кэш импортов:

vi.resetModules()

После этого модуль импортируется заново.


isolateModules

Позволяет выполнить импорт в изолированном контексте.

vi.isolateModules(async () => {
    const module = await import('./store.js')

    console.log(module)
})

Мокирование composables и hooks

Vue composables

vi.mock('./useAuth.js', () => {
    return {
        useAuth: () => ({
            isAuth: true
        })
    }
})

React hooks

vi.mock('./useTheme.js', () => {
    return {
        useTheme: () => ({
            theme: 'dark'
        })
    }
})

setupFiles для глобальных моков

Vitest поддерживает глобальные setup-файлы.

Конфигурация:

export default defineConfig({
    test: {
        setupFiles: [
            './tests/setup.js'
        ]
    }
})

Пример setup.js

import { vi } from 'vitest'

vi.stubEnv(
    'VITE_API_URL',
    'http://localhost:3000'
)

vi.stubGlobal(
    'fetch',
    vi.fn()
)

restoreMocks

Автоматическое восстановление моков:

export default defineConfig({
    test: {
        restoreMocks: true
    }
})

clearMocks

Автоматическая очистка истории вызовов:

export default defineConfig({
    test: {
        clearMocks: true
    }
})

mockReset

Полный сброс mock-функций:

export default defineConfig({
    test: {
        mockReset: true
    }
})

Отличия clearMocks, restoreMocks и mockReset

Опция История вызовов Реализация Оригинальная функция
clearMocks Сбрасывается Сохраняется Нет
mockReset Сбрасывается Удаляется Нет
restoreMocks Сбрасывается Оригинал Да

Проблемы неправильного мокирования

Хрупкие тесты

Чрезмерное количество моков приводит к:

  • тестированию mock-объектов вместо логики
  • потере интеграционных связей
  • ложным успешным тестам

Ошибка чрезмерной изоляции

Плохой пример:

vi.mock('./db.js')
vi.mock('./api.js')
vi.mock('./cache.js')
vi.mock('./logger.js')
vi.mock('./auth.js')

Тест почти ничего не проверяет.


Когда мокирование действительно необходимо

Моки оправданы при работе с:

  • сетью
  • файловой системой
  • базой данных
  • временем
  • случайными значениями
  • глобальным состоянием
  • внешними API
  • browser API

Рекомендуемая стратегия

Практически полезная структура тестирования:

  • unit-тесты с моками
  • integration-тесты с минимальным количеством моков
  • e2e-тесты без мокирования

Такой подход обеспечивает:

  • высокую скорость тестов
  • стабильность
  • реалистичность выполнения
  • надёжную проверку бизнес-логики