Моки и стабы

При тестировании схем валидации на основе Vest ключевая сложность возникает там, где правила начинают зависеть от внешнего мира: API, базы данных, таймеров, случайных значений, конфигурационных сервисов. Любая такая зависимость делает тесты нестабильными, медленными и плохо воспроизводимыми. Для изоляции поведения используются два базовых механизма — моки и стабы.

Стаб (stub) подменяет реальную реализацию функции фиксированным поведением. Мок (mock) не только подменяет реализацию, но и фиксирует факт вызова, его параметры и количество обращений. В контексте Vest это особенно важно, так как валидационные цепочки часто зависят от асинхронных проверок.


Особенности тестирования схем Vest

Валидационные схемы в Vest строятся декларативно:

  • группы правил выполняются последовательно или параллельно
  • поддерживаются асинхронные проверки
  • возможны условные ветвления
  • результат зависит от внешних данных

Типичный пример зависимости:

  • проверка уникальности email через API
  • загрузка правил валидации с сервера
  • проверка токена пользователя
  • динамическая бизнес-логика

Такие сценарии нельзя тестировать напрямую без изоляции, так как:

  • сетевые запросы нестабильны
  • задержки увеличивают время тестов
  • внешние сервисы недоступны в CI

Изоляция асинхронных правил через стабы

Асинхронные проверки в Vest часто оформляются через test с промисами:

import { create, test } from 'vest';

const suite = create((data) => {
  test('email', 'Email уже используется', async () => {
    const exists = await apiCheckEmail(data.email);
    if (exists) {
      throw new Error();
    }
  });
});

Чтобы сделать тест стабильным, функция apiCheckEmail заменяется стабом:

import * as api from '../api';

jest.spyOn(api, 'apiCheckEmail').mockResolvedValue(false);

Такой подход фиксирует поведение зависимости:

  • API всегда возвращает ожидаемый результат
  • тест проверяет только логику Vest-схемы
  • отсутствует сетевой фактор

Использование моков для проверки взаимодействий

Моки применяются там, где важно не только значение, но и сам факт вызова зависимости.

Пример: логирование ошибок валидации:

function logValidationError(field, message) {
  analytics.track('validation_error', {
    field,
    message,
  });
}

В схеме Vest:

import { create, test } from 'vest';

const suite = create(() => {
  test('username', 'Ошибка', () => {
    logValidationError('username', 'Ошибка');
    throw new Error();
  });
});

Тест с мокированием:

jest.mock('../analytics');

import analytics from '../analytics';

test('logs validation error', () => {
  suite();

  expect(analytics.track).toHaveBeenCalledWith(
    'validation_error',
    expect.objectContaining({
      field: 'username',
    })
  );
});

Мок позволяет:

  • проверить факт вызова
  • проверить структуру аргументов
  • отследить количество вызовов

Разделение ответственности между моками и стабами

Использование моков и стабов в тестах Vest требует строгого разделения ролей:

Стабы:

  • возвращают фиксированные значения
  • не фиксируют вызовы
  • используются для API, утилит, вычислений

Моки:

  • фиксируют взаимодействия
  • проверяют побочные эффекты
  • используются для логирования, аналитики, событий

Нарушение этого разделения приводит к:

  • дублированию проверок
  • хрупким тестам
  • избыточной детализации

Изоляция модулей схемы валидации

Vest-схема часто импортирует внешние зависимости:

import { create, test } from 'vest';
import { checkEmail } from './services/user';
import { sendMetric } from './metrics';

Для тестирования схема должна быть изолирована:

jest.mock('./services/user', () => ({
  checkEmail: jest.fn(),
}));

jest.mock('./metrics', () => ({
  sendMetric: jest.fn(),
}));

Далее управление поведением:

import { checkEmail } from './services/user';

checkEmail.mockResolvedValue(true);

Такой подход позволяет тестировать:

  • ветвления внутри Vest-схем
  • асинхронные условия
  • обработку ошибок без реальных сервисов

Моки асинхронных ошибок

Особый случай — имитация ошибок API:

checkEmail.mockRejectedValue(new Error('Network error'));

В Vest это приводит к ветке ошибок:

test('email', 'Ошибка проверки', async () => {
  try {
    await checkEmail();
  } catch (e) {
    throw new Error('Ошибка проверки');
  }
});

Тестирование такого поведения важно для:

  • устойчивости к сбоям сети
  • корректной обработки исключений
  • предсказуемого UX

Стабилизация времени и таймеров

Некоторые схемы используют задержки:

test('username', 'Слишком частые запросы', async () => {
  await new Promise((resolve) => setTimeout(resolve, 500));
  return checkUsername();
});

Для тестов таймеры стабилизируются:

jest.useFakeTimers();

jest.advanceTimersByTime(500);

Это позволяет:

  • избежать реальных задержек
  • синхронизировать асинхронные цепочки
  • контролировать выполнение теста

Контроль состояния вызовов в сложных схемах

Vest позволяет группировать тесты, что приводит к множественным вызовам зависимостей:

group('auth', () => {
  test('email', ...);
  test('password', ...);
});

При использовании моков важно учитывать:

  • количество вызовов одной зависимости
  • порядок вызовов
  • повторное выполнение при изменении состояния

Пример проверки:

expect(checkEmail).toHaveBeenCalledTimes(2);

Ошибки при использовании моков и стабов

Распространённые проблемы:

  1. Чрезмерное мокирование внутренних функций Приводит к тестированию реализации, а не поведения

  2. Стабирование логики, которая должна быть частью теста Скрывает реальные дефекты

  3. Неполное восстановление моков между тестами Создаёт утечки состояния

  4. Мокирование всей схемы Vest вместо зависимостей Делает тест бессмысленным


Организация тестовой архитектуры

При работе с Vest важно выстроить структуру:

  • зависимости выносятся в отдельные модули

  • API и побочные эффекты централизуются

  • бизнес-логика отделяется от инфраструктуры

  • тесты делятся на:

    • проверку схемы
    • проверку взаимодействий
    • проверку ошибок

Моки и стабы становятся инструментом управления границами системы, а не средством подмены логики.


Асинхронные цепочки и комбинирование стабов

Сложные сценарии включают несколько зависимостей:

const exists = await checkEmail();
const allowed = await checkPermissions();

Стабирование:

checkEmail.mockResolvedValue(false);
checkPermissions.mockResolvedValue(true);

Это позволяет проверять:

  • комбинации условий
  • ветвления внутри Vest
  • итоговый статус валидации

Применение моков для анализа поведения схем

Моки дают возможность анализировать не только результат, но и поведение:

  • сколько раз запускалась проверка
  • какие поля триггерили API
  • в каком порядке выполнялись тесты
  • какие ветки логики были активированы

Это особенно важно для сложных схем, где простого сравнения результата недостаточно и требуется понимание внутреннего исполнения.