Отладка тестов

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

  • определить, почему тест не запускается;
  • понять источник ошибки;
  • проверить корректность состояния suite;
  • выявить гонки асинхронных проверок;
  • проанализировать поведение only, skip, group;
  • контролировать кеширование и повторные прогоны;
  • отслеживать изменение статусов тестов.

Анализ структуры suite

Основной объект в Vest — validation suite. Именно он управляет запуском тестов и хранением состояния.

Пример базового suite:

import { create, test, enforce } from 'vest';

const suite = create((data = {}) => {
  test('email', 'Некорректный email', () => {
    enforce(data.email).matches(/@/);
  });

  test('password', 'Минимум 6 символов', () => {
    enforce(data.password).longerThanOrEquals(6);
  });
});

Для отладки важно понимать:

  • какие тесты уже были выполнены;
  • какие завершились ошибкой;
  • какие находятся в состоянии pending;
  • какие были пропущены;
  • какие перезапускаются повторно.

Получение состояния suite

Vest предоставляет API для анализа внутреннего состояния.

Использование get()

const result = suite(data);

console.log(result.get());

Пример структуры:

{
  valid: false,
  pending: false,
  tested: ['email', 'password'],
  errors: {
    email: ['Некорректный email']
  }
}

Ключевые свойства

Свойство Назначение
valid Общий статус валидности
pending Есть ли незавершённые async-тесты
tested Какие поля были проверены
errors Ошибки по полям
warnings Предупреждения
groups Результаты групп

Проверка выполнения конкретного теста

При сложной логике часть тестов может не запускаться.

Диагностика через hasErrors

const result = suite(data);

console.log(result.hasErrors('email'));

Проверка выполнения

console.log(result.tested('email'));

Если вывод:

false

значит тест не выполнялся.


Отладка условий

Частая проблема — неверные условия запуска.

Пример ошибки

if (data.role === 'admin') {
  test('access', 'Нет доступа', () => {
    enforce(data.access).isNotEmpty();
  });
}

Если role не равен admin, тест вообще не создаётся.

Диагностика

console.log(data.role);

или:

console.log(result.tested('access'));

Анализ skip и only

Vest позволяет ограничивать набор выполняемых тестов.

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

only('email');

test('email', 'Ошибка email', () => {
  enforce(data.email).matches(/@/);
});

test('password', 'Ошибка password', () => {
  enforce(data.password).longerThanOrEquals(6);
});

Выполнится только email.

Типичная проблема

Разработчик забывает удалить only.

Диагностика

Проверка списка выполненных тестов:

console.log(result.get().tested);

Анализ skip

skip('password');

test('password', 'Ошибка', () => {
  enforce(data.password).isNotEmpty();
});

Диагностика

console.log(result.tested('password'));

Отладка асинхронных тестов

Асинхронные проверки — наиболее частый источник ошибок.

Базовый async-test

test('email', 'Email уже существует', async () => {
  const exists = await api.checkEmail(data.email);

  enforce(exists).isFalsy();
});

Анализ состояния pending

Vest отслеживает незавершённые операции.

const result = suite(data);

console.log(result.pending());

Если вывод:

true

значит async-проверки ещё выполняются.


Проверка зависших async-тестов

Проблема:

test('email', 'Ошибка', async () => {
  await fetch('/api');
});

Ошибка возникает, если Promise никогда не завершается.

Диагностика

console.log(result.pending());

Если состояние постоянно true, async-тест завис.


Логирование async-потока

Полезный подход — добавление логов.

test('email', 'Ошибка', async () => {
  console.log('START');

  const response = await fetch('/api');

  console.log('RESPONSE');

  const json = await response.json();

  console.log('JSON');

  enforce(json.valid).isTruthy();
});

Отладка race conditions

При быстром вводе данных старые запросы могут завершаться позже новых.

Проблемный сценарий

suite({ email: 'a@mail.com' });
suite({ email: 'ab@mail.com' });
suite({ email: 'abc@mail.com' });

Первый запрос может завершиться последним.


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

Vest автоматически умеет отменять устаревшие проверки.

Пример

import { create, test } from 'vest';

const suite = create(async data => {
  test('email', 'Ошибка', async () => {
    const response = await api.validate(data.email);

    return response.valid;
  });
});

Диагностика

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

console.count('validation request');

Отладка memoization

Vest кеширует результаты тестов.

Пример

suite({ email: 'test@mail.com' });
suite({ email: 'test@mail.com' });

Некоторые тесты могут не перезапускаться.


Проверка повторного выполнения

test('email', 'Ошибка', () => {
  console.log('RUN EMAIL TEST');

  enforce(data.email).matches(/@/);
});

Если лог не появляется повторно — сработал кеш.


Принудительный rerun

suite.reset();

После этого:

suite(data);

все тесты будут выполнены заново.


Отладка групп тестов

Группы помогают организовывать валидацию.

Пример

group('auth', () => {
  test('email', 'Ошибка email', () => {});
  test('password', 'Ошибка password', () => {});
});

Анализ групп

const result = suite(data);

console.log(result.get().groups);

Пример:

{
  auth: {
    valid: false
  }
}

Отладка динамических тестов

Vest поддерживает генерацию тестов в циклах.

Пример

data.items.forEach((item, index) => {
  test(`item_${index}`, 'Ошибка', () => {
    enforce(item.title).isNotEmpty();
  });
});

Типичная ошибка

Изменение индексов:

[
  { id: 1 },
  { id: 2 }
]

после удаления:

[
  { id: 2 }
]

Тест item_1 превращается в item_0.

Это приводит к конфликтам состояния.


Правильный подход

Использование стабильных идентификаторов:

data.items.forEach(item => {
  test(`item_${item.id}`, 'Ошибка', () => {
    enforce(item.title).isNotEmpty();
  });
});

Отладка warn-режима

Vest поддерживает предупреждения вместо ошибок.

Пример

import { warn } from 'vest';

test.memo('password_strength', () => {
  warn();
});

Проверка warnings

console.log(result.get().warnings);

Диагностика вложенных условий

Сложная логика часто создаёт трудноуловимые ошибки.

Пример

if (data.enabled) {
  if (data.role === 'admin') {
    test('access', 'Ошибка', () => {
      enforce(data.access).isNotEmpty();
    });
  }
}

Упрощение отладки

Полезно выносить условия:

const shouldValidateAccess =
  data.enabled &&
  data.role === 'admin';

console.log(shouldValidateAccess);

if (shouldValidateAccess) {
  test('access', 'Ошибка', () => {
    enforce(data.access).isNotEmpty();
  });
}

Анализ ошибок enforce

Большинство validation-ошибок возникает внутри enforce.

Пример

enforce(data.age).greaterThan(18);

Проверка входящих данных

console.log(typeof data.age);
console.log(data.age);

Частая проблема типов

data.age = '20';

Строка вместо числа.


Исправление

enforce(Number(data.age)).greaterThan(18);

Отладка null и undefined

Проблемный код

enforce(data.user.email).matches(/@/);

Ошибка:

Cannot read property 'email' of undefined

Безопасная проверка

enforce(data.user?.email).matches(/@/);

или:

if (data.user) {
  enforce(data.user.email).matches(/@/);
}

Использование try/catch

При сложной async-валидации полезна ручная обработка ошибок.

Пример

test('email', 'Ошибка сервера', async () => {
  try {
    const response = await api.validate(data.email);

    enforce(response.valid).isTruthy();
  } catch (e) {
    console.error(e);

    throw e;
  }
});

Отладка через breakpoints

Vest отлично работает с debugger.

Пример

test('email', 'Ошибка', () => {
  debugger;

  enforce(data.email).matches(/@/);
});

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


Анализ порядка выполнения

Иногда тесты запускаются не в ожидаемом порядке.

Диагностика

test('a', 'A', () => {
  console.log('A');
});

test('b', 'B', () => {
  console.log('B');
});

test('c', 'C', () => {
  console.log('C');
});

Отладка revalidation

Vest повторно валидирует только изменённые поля.

Пример

suite({ email: 'a' });
suite({ email: 'ab' });

Может выполняться только часть тестов.


Проверка зависимостей

test('confirm', 'Пароли не совпадают', () => {
  enforce(data.confirm).equals(data.password);
});

Если изменяется password, тест confirm тоже должен обновляться.


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

import { include } from 'vest';

include('confirm');

Отладка состояния формы

При интеграции с React состояние формы может расходиться с состоянием suite.

Диагностика

console.log(formState);
console.log(result.get());

Сравнение помогает обнаружить:

  • устаревшие значения;
  • пропущенные rerender;
  • несинхронные обновления;
  • ошибки debounce;
  • повторные async-вызовы.

Отладка reset

Полный reset

suite.reset();

Reset отдельного поля

suite.resetField('email');

Проверка reset

console.log(result.get());

После reset ошибки должны исчезнуть.


Логирование состояния suite

Полезная техника — централизованный логгер.

Пример

function debugSuite(result) {
  console.log({
    valid: result.isValid(),
    pending: result.pending(),
    errors: result.getErrors(),
    warnings: result.getWarnings(),
    tested: result.get().tested
  });
}

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

const result = suite(data);

debugSuite(result);

Отладка интеграции с React

Частая проблема

const result = suite(formData);

выполняется на каждом render.

Это приводит к:

  • лишним async-запросам;
  • постоянным rerun;
  • мерцанию ошибок;
  • деградации производительности.

Диагностика повторных render

console.count('FORM RENDER');

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

const result = useMemo(() => {
  return suite(formData);
}, [formData]);

Анализ производительности

При большом количестве тестов возникают задержки.

Диагностика времени выполнения

console.time('validation');

suite(data);

console.timeEnd('validation');

Поиск тяжёлых тестов

test('email', 'Ошибка', () => {
  console.time('email test');

  enforce(data.email).matches(/@/);

  console.timeEnd('email test');
});

Проверка бесконечных циклов

Проблемный сценарий:

useEffect(() => {
  setState(suite(data));
}, [data]);

где suite вызывает изменение data.


Диагностика

console.count('VALIDATION');

Если счётчик растёт бесконечно — существует цикл обновлений.


Отладка compose

Vest поддерживает композицию suite.

Пример

const authSuite = create(() => {});
const profileSuite = create(() => {});

Диагностика compose

console.log(authSuite(data).get());
console.log(profileSuite(data).get());

Раздельная проверка помогает локализовать источник ошибки.


Практика изоляции ошибок

Эффективная стратегия отладки:

  1. Отключение async-тестов.
  2. Проверка sync-валидации.
  3. Возврат async-логики.
  4. Проверка групп.
  5. Анализ rerun.
  6. Проверка reset.
  7. Анализ кеширования.
  8. Сравнение входящих данных.
  9. Проверка зависимостей.
  10. Анализ порядка выполнения.

Типичные ошибки в Vest

Тест не запускается

Причины:

  • skip;
  • условие if;
  • ошибка в group;
  • кеширование;
  • only.

Async-test зависает

Причины:

  • незавершённый Promise;
  • ошибка fetch;
  • отсутствие await;
  • race condition.

Ошибки не исчезают

Причины:

  • отсутствие reset;
  • stale state;
  • неправильные ключи;
  • кеш suite.

Тест выполняется слишком часто

Причины:

  • rerender React;
  • отсутствие memoization;
  • изменение объекта data;
  • пересоздание suite.

Ошибки появляются у неверного поля

Причины:

  • динамические индексы;
  • одинаковые имена тестов;
  • конфликт group;
  • некорректные include-зависимости.