Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

Невнятный тест.

Невнятный тест — это тест, в котором читатель по одному только методу теста не может понять, какой сценарий настроен, какое поведение проверяется и какой результат ожидается, потому что намерение погребено под избытком деталей, недостатком контекста или логикой, спрятанной где-то ещё.

##Признаки и симптомы

Вы читаете тест и всё равно не можете ответить на три вопроса: какова настройка, что именно проверяется и каков ожидаемый результат? Цепочка причина/следствие скрыта. Месарос группирует причины на «слишком много информации» и «слишком мало информации». Типичные приметы:

  • Eager Test (жадный тест) — один метод теста проверяет много несвязанных вариантов поведения, поэтому никакое единое намерение не ясно.
  • Mystery Guest (таинственный гость) — фикстура или ожидаемые значения живут вне теста (файл, общая запись в БД, фикстура, загружаемая по имени), поэтому вы не видите, почему проверка должна пройти.
  • General Fixture (общая фикстура) — большой общий beforeEach/фабрика строит куда больше, чем нужно этому тесту; немногие значимые поля тонут в шуме.
  • Irrelevant Information (несущественная информация) — страницы данных настройки или гигантский снимок, где на деле важны лишь одно-два поля. Подробно — в статье «Многословный тест».
  • Hard-Coded Test Data (захардкоженные тестовые данные) — «магические» литералы (42, "a3f9-...", userId=7) без именованного смысла, повторяющиеся в настройке и в проверках. См. «Тест с магическими числами».
  • Indirect Testing (косвенное тестирование) — тест тыкает в проверяемую систему через несколько других объектов, затемняя то, что на самом деле проверяется.
// Невнятно: Eager + Mystery Guest + Irrelevant Information
test('user', async () => {
  const data = loadFixture('users.json');        // таинственный гость: значения живут в файле
  const svc = new UserService(data, cfg, clock); // общая фикстура: большая часть не используется
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });

  expect(u.id).toBeDefined();
  expect(u.email).toContain('@');
  expect(await svc.count()).toBe(data.length + 1); // почему это число? ответ — в файле
  expect(await svc.login(u.email, 'p@ss')).toBe(true); // несвязанное второе поведение
});

Здесь невозможно понять, что доказывает тест, не открыв users.json, и он на деле проверяет создание и вход за один раз.

##Причины проблемы

Почему это происходит

  • Тесты разрастаются наслоением: разработчик добавляет «ещё одну проверку» в существующий тест вместо того, чтобы написать новый, порождая Eager Test (жадный тест).
  • Совместное использование настройки кажется эффективным, поэтому единая широкая фикстура или beforeEach переиспользуется повсюду (General Fixture, общая фикстура), а внешние файлы/наполнение БД загружаются по имени (Mystery Guest, таинственный гость).
  • Копирование правдоподобно выглядящих данных тащит за собой десятки несущественных полей и «магических чисел».
  • Чрезмерное использование моков или проход через множество сотрудников-зависимостей превращает модульный тест в Indirect Testing (косвенное тестирование).

Почему это вредно

  • Читаемость: тест — это документация задуманного поведения. Если читатель не может восстановить цепочку «Настройка → Действие → Проверка», эта документационная ценность теряется.
  • Сопровождаемость: когда невнятный тест падает, вы не знаете, какое поведение сломалось и ошибка в тесте или в коде, поэтому изменения медленны и рискованны.
  • Надёжность: Mystery Guest и General Fixture связывают тест с внешним/общим состоянием, вызывая нестабильные или зависящие от порядка падения (и нарушая свойство «свежий, детерминированный» хорошего модульного теста).
  • Ложная уверенность: Eager Test маскирует покрытие — падение в начале метода обрывает выполнение последующих проверок, поэтому поведение, которое вы думаете, что протестировано, может никогда не выполниться. И, как отмечает Месарос, ошибки в коде проще спрятать в невнятном тесте, порождая Buggy Tests (баговые тесты), которые проходят по неверным причинам.

##Лечение

Единого лекарства от невнятного теста нет: «невнятность» — симптом, у которого несколько причин. Сначала поставьте диагноз, затем примените средство против найденной причины — у каждой есть своя статья с подробными рефакторингами.

  1. Устройте «холодное чтение». Попросите человека, не видевшего код, по одному лишь телу теста назвать подготовку, действие и ожидаемый результат. Там, где ему пришлось открыть другой файл или гадать, и находится причина.
  2. Несколько несвязанных вариантов поведения в одном тесте? Это нетерпеливый тест: разбейте его на сфокусированные тесты, каждый из которых назван по проверяемому поведению. Заодно раннее падение перестанет скрывать последующие проверки.
  3. Ожидаемые значения обосновываются внешним файлом, сидом или общей записью? Это таинственный гость: перенесите в тест данные, от которых зависит проверка.
  4. Широкий общий beforeEach строит куда больше, чем нужно тесту? Это универсальная фикстура: перейдите на минимальную свежую фикстуру для каждого теста.
  5. Страницы несущественной подготовки или поштучные проверки полей? Это многословный тест: спрячьте шум за методами создания и билдерами и сравнивайте с ожидаемым объектом — механика подробно разобрана там.
  6. Необъяснённые литералы? См. «Тест с магическими числами»: дайте значениям имена, чтобы проверка объясняла сама себя.
  7. Проверяемая система доступна только через цепочку других объектов (косвенное тестирование)? Тестируйте её через её собственный интерфейс или вынесите логику так, чтобы её можно было проверить напрямую.
// До (невнятно)
test('user', async () => {
  const data = loadFixture('users.json');
  const svc = new UserService(data, cfg, clock);
  const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
  expect(await svc.count()).toBe(data.length + 1);
});

// После: одно намерение, фикстура встроена, данные именованы
test('create() persists a new user', async () => {
  const svc = userServiceWith([]);                 // минимальная, свежая фикстура
  const newUser = aUser({ email: '[email protected]' }); // строитель задаёт значения шума по умолчанию

  const created = await svc.create(newUser);

  expect(created.email).toBe('[email protected]');
  expect(await svc.count()).toBe(1);               // самоочевидно, без внешнего файла
});

Стремитесь к форме AAA (Arrange-Act-Assert, Подготовка-Действие-Проверка): читатель должен ухватить сценарий, не покидая метода теста.

##Обнаружение анализаторами

  • eslint-jest jest/max-expects — Ограничивает число вызовов expect() в одном тесте
  • eslint-vitest vitest/max-expects — Ограничивает число вызовов expect() в одном тесте
  • eslint-jest jest/no-large-snapshots — Запрещает большие снапшоты
  • eslint-vitest vitest/no-large-snapshots — Запрещает большие снапшоты
  • eslint-jest jest/max-nested-describe — Ограничивает глубину вложенности блоков describe()