Обработка исключений в хуках

В системе тестирования и валидации Vest хуки выполняются в рамках жизненного цикла сьютов и тестов. Исключения в них можно разделить на несколько категорий:

1. Синхронные ошибки выполнения

  • выбрасываются через throw
  • возникают при логических ошибках внутри функции
  • перехватываются системой выполнения сьюта

2. Асинхронные ошибки

  • возникают в async хуках
  • проявляются как Promise rejection
  • требуют явного ожидания выполнения (await)

3. Ошибки сторонних зависимостей

  • API-запросы
  • работа с файловой системой
  • обращения к внешним сервисам

4. Ошибки валидации

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

Поведение хуков при возникновении исключений

Хуки в Vest выполняются в строго определённой последовательности, и поведение при ошибках зависит от типа хука и контекста.

beforeEach и afterEach

Эти хуки выполняются вокруг каждого теста:

  • ошибка в beforeEach прерывает выполнение текущего теста
  • ошибка в afterEach не отменяет результат теста, но фиксируется в результате сьюта

Пример:

import { suite } from 'vest';

const s = suite('example', () => {
  beforeEach(() => {
    if (!globalThis.config) {
      throw new Error('Config not initialized');
    }
  });

  test('field', 'validation message', () => {
    // тест не выполнится, если beforeEach выбросил исключение
  });

  afterEach(() => {
    cleanupTempState();
  });
});

Обработка исключений в beforeAll и afterAll

beforeAll

Используется для подготовки окружения. Исключения здесь критичны:

  • при ошибке выполнение всего сьюта прекращается
  • все тесты помечаются как не выполненные

Рекомендуется изолировать потенциально опасные операции:

beforeAll(async () => {
  try {
    await connectToDatabase();
  } catch (err) {
    throw new Error('Database connection failed: ' + err.message);
  }
});

afterAll

Используется для освобождения ресурсов:

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

Практика:

afterAll(async () => {
  try {
    await closeDatabase();
  } catch (err) {
    console.error('Cleanup failed:', err);
  }
});

Изоляция ошибок внутри тестов Vest

Внутри test ошибки могут возникать как через enforce, так и через обычные исключения.

Синхронные ошибки

test('username', 'Invalid username', () => {
  if (typeof value !== 'string') {
    throw new Error('Username must be string');
  }
});

Такие ошибки автоматически регистрируются как провал теста.


Ошибки через enforce

enforce генерирует структурированные ошибки:

test('email', 'Invalid email', () => {
  enforce(value).isNotEmpty();
  enforce(value).matches(/@/);
});

Если условие не выполнено, Vest не выбрасывает необработанное исключение, а записывает ошибку в результат сьюта.


Асинхронные хуки и обработка ошибок

Асинхронные операции требуют особого внимания.

Ошибка без обработки

beforeEach(async () => {
  await fetchConfig(); // может reject
});

Если fetchConfig завершится с ошибкой, сьют прервётся.


Явная обработка ошибок

beforeEach(async () => {
  try {
    await fetchConfig();
  } catch (err) {
    throw new Error('Failed to load config');
  }
});

Гарантированное завершение с afterEach

afterEach(async () => {
  try {
    await rollbackChanges();
  } catch (err) {
    console.warn('Rollback failed:', err);
  }
});

Поглощение ошибок внутри хуков

Иногда необходимо не прерывать выполнение сьюта при ошибке вспомогательной логики.

Подавление ошибки

beforeEach(() => {
  try {
    initOptionalService();
  } catch (err) {
    // ошибка игнорируется
  }
});

Такой подход используется только для некритичных зависимостей.


Проброс ошибок в структуру Vest

Vest различает:

  • ошибки исполнения (runtime errors)
  • ошибки валидации (validation errors)

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

test('profile', 'Profile validation failed', async () => {
  try {
    await validateProfile(value);
  } catch (err) {
    throw new Error('Profile service error: ' + err.message);
  }
});

Ошибки в кастомных утилитах внутри хуков

Часто хуки используют вспомогательные функции, что увеличивает риск неконтролируемых исключений.

Проблема непрозрачного падения

beforeEach(() => {
  setup(); // внутри может быть throw
});

Контролируемая обёртка

function safeSetup() {
  try {
    setup();
  } catch (err) {
    throw new Error('Setup failed');
  }
}

beforeEach(() => {
  safeSetup();
});

Влияние исключений на порядок выполнения

При возникновении ошибки:

  • текущий тест может быть пропущен
  • последующие тесты продолжают выполняться (если ошибка не критическая для beforeAll)
  • afterEach выполняется даже при падении теста
  • afterAll выполняется в любом случае

Асинхронные цепочки и накопление ошибок

При последовательных async-хуках важно учитывать:

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

Пример проблемного сценария:

beforeEach(async () => {
  await stepOne();
  await stepTwo(); // падение здесь оставляет частичное состояние
});

Решение — атомарные операции:

beforeEach(async () => {
  try {
    await runAllSetupSteps();
  } catch (err) {
    throw new Error('Setup chain failed');
  }
});

Стратегии устойчивой обработки исключений

1. Изоляция побочных эффектов

Любые I/O операции выполняются в try/catch блоках:

afterEach(async () => {
  try {
    await fsCleanup();
  } catch (err) {
    logError(err);
  }
});

2. Минимизация логики в хуках

Хуки должны оставаться тонкими обёртками над функциями:

beforeEach(() => initContext());
afterEach(() => destroyContext());

3. Централизация обработки ошибок

function handleHookError(context, err) {
  throw new Error(`[${context}] ${err.message}`);
}

beforeEach(() => {
  try {
    init();
  } catch (err) {
    handleHookError('beforeEach:init', err);
  }
});

4. Контроль непредвиденных ошибок

process.on('unhandledRejection', (err) => {
  console.error('Unhandled rejection in Vest suite:', err);
});

Поведение при каскадных ошибках

Если несколько хуков выбрасывают исключения:

  • Vest фиксирует каждую ошибку отдельно
  • итоговый результат содержит агрегированную информацию
  • критические ошибки могут блокировать выполнение сьюта

Практика безопасной работы с хуками

Основные принципы:

  • не допускать необработанных async rejection
  • не выполнять критическую логику без try/catch
  • разделять подготовку и валидацию
  • минимизировать side-effects в beforeEach
  • избегать скрытых зависимостей между тестами