ConstructiCat Logo
CodeBust.
Browse section ▾

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

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

##Signs and Symptoms

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

  • 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, и он на деле проверяет создание и вход за один раз.

##Reasons for the Problem

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

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

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

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

##Treatment

Сделайте так, чтобы каждый тест рассказывал самодостаточную историю, намерение которой видно в теле теста.

  1. Проверяйте одно условие на тест. Разбейте Eager Test на сфокусированные тесты, каждый названный по поведению, которое он проверяет. Это также устраняет маскировку покрытия.
  2. Встройте нужную фикстуру (уберите Mystery Guest). Постройте данные, от которых зависит тест, в самом тесте или через явный, именованный метод создания/строитель — не загружайте анонимные файлы или общие строки БД.
  3. Используйте минимальную / свежую фикстуру. Стройте только то, что нужно этому тесту; замените широкий общий beforeEach строителем, который задаёт значения по умолчанию для шума и позволяет каждому тесту задать лишь проверяемое поле.
  4. Именуйте свои данные. Замените «магические» литералы раскрывающими намерение константами/переменными, чтобы проверка объясняла себя сама.
  5. Проверяйте смысл, а не всё подряд. Предпочитайте прицельные проверки гигантскому снимку; если делаете снимок, держите его маленьким и пригодным к ревью, чтобы значимые факты не утонули в несущественном выводе.
  6. Скрывайте механику, а не намерение. Уберите побочную обвязку в хорошо названные вспомогательные/утилитарные методы теста, чтобы тело теста читалось как настройка → действие → ожидание.
// До (невнятно)
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: 'ada@example.com' }); // строитель задаёт значения шума по умолчанию

  const created = await svc.create(newUser);

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

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

##Detected by