ConstructiCat Logo
CodeBust.
Browse section ▾

Рулетка проверок.

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

##Signs and Symptoms

Один тест содержит череду голых проверок без поясняющих сообщений и без ясной единственной цели. Когда он краснеет, отчёт о падении (особенно строка-сводка CI или общий помощник для проверок) сообщает, что что-то сломалось, но не какая проверка и почему — вы играете в «рулетку», чтобы найти виновника.

Выдающие признаки:

  • Много вызовов expect(...) / assert(...) в одном тесте, ни один из которых не несёт описательного сообщения или метки.
  • Имя теста обобщённое ('works', 'user profile', 'test register'), так что падение само по себе ничего не сообщает.
  • Проверки охватывают несколько несвязанных аспектов (Eager Test, переиспользующий одну фикстуру, чтобы проверить всё разом).
  • Падение вынуждает вас лезть в отладчик или к номерам строк, чтобы понять, что вообще проверялось.
  • Поскольку большинство проверок отказывают быстро, первый провал обрывает остальные, поэтому вы никогда не увидите последующие проверки за один прогон.
test('user profile', () => {
  const user = createUser({ name: 'Ada', age: 36 });
  expect(user.name).toBe('Ada');
  expect(user.age).toBe(36);
  expect(user.isAdult).toBe(true);   // если красной окажется ИМЕННО эта,
  expect(user.slug).toBe('ada');     // отчёт просто скажет
  expect(user.roles).toContain('member'); // «expected false to be true»
  expect(user.createdAt).toBeInstanceOf(Date);
});

Замечание: современные раннеры (Jest, Vitest) печатают упавшую строку и diff, что смягчает проблему «какая строка?». Запах всё равно кусается, когда тест смешивает цели, имеет расплывчатое имя, перебирает проверки в цикле, прячет их за общими помощниками или выполняется там, где выживает лишь сводное сообщение.

##Reasons for the Problem

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

  • Eager Test / переиспользование фикстуры. Дорогая настройка соблазняет навалить проверок «раз уж я здесь» вместо написания второго теста.
  • Проверка объекта целиком, сделанная сложным путём. Проверка объекта поле за полем естественно порождает длинную череду недокументированных проверок.
  • Рост копипастом. Тесты со временем обрастают проверками, и никто их не разделяет.
  • Исторический инструментарий. Классические проверки xUnit сообщали лишь успех/провал, поэтому проверка без метки не давала контекста при падении — отсюда и исходный запах у Месароса.
  • Давление сроков. Один большой тест кажется быстрее в написании, чем несколько сфокусированных.

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

  • Диагностируемость / надёжность. По данным testsmells.org, «множественные операторы проверки в методе теста без описательного сообщения ухудшают читаемость/понятность/сопровождаемость, так как невозможно понять причину падения». Вы тратите время на поиск того, какая проверка сработала.
  • Скрытые дефекты (ложная уверенность). Проверки с быстрым отказом останавливаются на первом провале, поэтому последующие проверки никогда не выполняются. Вы чините одну, перезапускаете, находите следующую — несколько багов замаскированы, и «зелёный после одной правки» ощущается безопаснее, чем есть.
  • Читаемость. Тест перестаёт документировать единственное поведение; его намерение погребено в списке, а обобщённое имя теста ничего не добавляет.
  • Сопровождаемость. Неясно, относится ли новая проверка к этому тесту или к новому, поэтому куча продолжает расти, а несвязанные аспекты оказываются сцеплены.

##Treatment

Стремитесь к принципу, стоящему за тестом с одним условием (Single-Condition Test): каждый тест проверяет одно поведение и может упасть ровно по одной причине.

  1. Разделяйте по цели. Разбейте универсальный тест на сфокусированные тесты с описательными именами. Тогда имя и есть сообщение о падении.
  2. Сворачивайте проверки поле за полем в одну проверку. Используйте toEqual / expect.objectContaining / просмотренный снимок, чтобы N проверок стали одним осмысленным diff.
  3. Когда группировка по-настоящему связна, помечайте проверки. У expect в Jest/Vitest нет аргумента-сообщения, поэтому используйте параметр-сообщение из node:assert, expect(actual, message) в Vitest или jest-expect-message.
  4. Хотите, чтобы все проверки выполнялись и отчитывались вместе? Предпочтите разделение; иначе используйте мягкие проверки (expect.soft в Vitest), чтобы один провал не скрывал остальные.
  5. Защититесь от регрессии, ограничив число проверок на тест (см. детекторы).

До — рулетка проверок:

test('register user', () => {
  const res = register({ email: 'a@b.com', age: 36 });
  expect(res.ok).toBe(true);
  expect(res.user.email).toBe('a@b.com');
  expect(res.user.isAdult).toBe(true);
  expect(res.welcomeEmailSent).toBe(true);
});

После — одно поведение на тест, с проверкой объекта целиком:

describe('register', () => {
  it('accepts a valid adult signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
  });

  it('stores the normalized user record', () => {
    const { user } = register({ email: 'a@b.com', age: 36 });
    expect(user).toEqual(
      expect.objectContaining({ email: 'a@b.com', isAdult: true }),
    );
  });

  it('sends a welcome email on signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
  });
});

Если приходится оставить один тест, сделайте каждую проверку самоописывающей:

import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');

##Detected by

  • eslint-jest jest/max-expectsОтмечает основанный на подсчёте суррогат Assertion Roulette: сообщает, когда тест превышает N вызовов expect() (по умолчанию 5). Документация отмечает, что большее число проверок склонно смешивать несколько целей.
  • eslint-vitest vitest/max-expectsАналог для Vitest: устанавливает максимальное число проверок expect() на тест, отлавливая тесты, в которые упихано слишком много проверок.
  • sonar java:S5961SonarSource «Test methods should not contain too many assertions» — ограничивает число проверок на тест (по умолчанию 25 для JUnit/AssertJ), стандартный суррогат этого запаха для статического анализа.