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

Многословный тест.

Тест, который использует гораздо больше кода, чем нужно для изложения своего сценария, погребая ту единственную связку «причина-следствие», которую он проверяет, под шаблонной подготовкой, нерелевантными данными и проверками поле-за-полем.

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

В xUnit Test Patterns Месароша многословный тест — не отдельный запах: он указан там как другое название (псевдоним) «Невнятного теста». Эта страница подробно разбирает одно семейство его причин — объём: конструирование объектов прямо в тесте, несущественную информацию и поштучные проверки полей. Для остальных причин (таинственный гость, нетерпеливый тест, универсальная фикстура…) начните со статьи о невнятном тесте.

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

  • Тестовый метод длинный (каталог Test Smells использует грубый порог в >30 строк) или не помещается на один экран.
  • Большой блок arrange строит объекты поле-за-полем, и многое из этого нерелевантно проверяемому поведению.
  • Множество захардкоженных литералов без очевидной связи между входами и проверяемыми выходами.
  • Результат проверяется грудой проверок отдельных полей вместо одного осмысленного сравнения.
  • Читая проверки, вы не можете легко сопоставить, какой вход вызвал какое ожидаемое значение.
test('places an order for a returning customer', () => {
  const address = new Address();
  address.street = '123 Main St';
  address.city = 'Springfield';
  address.zip = '00000';                          // нерелевантно этому тесту
  address.country = 'US';

  const customer = new Customer();
  customer.id = 42;
  customer.firstName = 'Ada';
  customer.lastName = 'Lovelace';
  customer.email = '[email protected]';
  customer.address = address;
  customer.createdAt = new Date('2020-01-01');     // шум

  const product = new Product();
  product.sku = 'SKU-1';
  product.name = 'Widget';
  product.price = 9.99;

  const cart = new Cart();
  cart.add(product, 2);

  const order = orderService.placeOrder(customer, cart);

  expect(order.status).toBe('CONFIRMED');          // важны только статус и итог
  expect(order.lineItems.length).toBe(1);
  expect(order.lineItems[0].sku).toBe('SKU-1');
  expect(order.lineItems[0].quantity).toBe(2);
  expect(order.total).toBe(19.98);
  expect(order.customerId).toBe(42);
});

Восемнадцать строк конструирования объектов и шесть поштучных проверок полей — всего 32 строки — окружают сценарий, который сводится к фразе «два виджета по 9,99 дают подтверждённый заказ на 19,98».

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

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

  • Inline-конструирование с обязательными полями. Создание реальных объектов через конструкторы/сеттеры вынуждает поставлять данные, до которых тесту нет дела, только ради того, чтобы код скомпилировался и запустился. Мезарос отмечает, что уровень детализации, нужный для исполняемости теста, может сделать его «настолько многословным, что его трудно понять».
  • Рост через копипаст. Тест начинается маленьким, затем каждый новый сценарий создаётся копированием предыдущего и подкручиванием значения, так что шум накапливается.
  • Нет общих хелперов конструирования. Без Creation Methods, билдеров или фикстур каждый тест заново заявляет весь граф объектов.
  • Захардкоженные данные и проверка поле-за-полем. Писать литералы повсюду и проверять каждое свойство кажется «основательным», но многократно раздувает число строк.

Чем это вредит

  • Читаемость. Как и любой невнятный тест, он прячет связь «причина — следствие» между фикстурой и результатом, а ведь в этом весь смысл теста, основанного на примере.
  • Сопровождаемость. Тест в 30+ строк обычно несёт «несколько обязанностей», поэтому небольшое изменение в продакшене расходится по многим строкам во многих тестах. Массовая inline-подготовка к тому же плодит дублирование.
  • Ложная уверенность. Длинные, шумные тесты прячут баги на виду: неверный литерал или сбившаяся проверка легко проскальзывают, а многословие заставляет ревьюеров скользить взглядом. Объём выглядит строгим, ничего при этом не доказывая дополнительно.
  • Надёжность. Чем больше нерелевантного состояния фиксирует тест, тем вероятнее он сломается по причинам, не связанным с его замыслом (избыточная специализация), давая хрупкие, малосодержательные падения.

##Лечение

Вытолкните шум из тела теста, чтобы на виду остались только те переменные, что определяют поведение. Конкретные приёмы (все из xUnit Test Patterns):

  1. Извлеките Creation Methods / используйте билдер. Замените inline-конструирование объектов выразительно названным хелпером, который поставляет разумные значения по умолчанию; передавайте только те значения, которые тесту действительно важны (Parameterized Creation Method). Это убивает и шаблонный код, и Irrelevant Information.
  2. Задавайте нерелевантные данные по умолчанию внутри хелпера, сигнализируя «эти значения не влияют на исход».
  3. Замените проверки поле-за-полем на Expected Object (toEqual/toMatchObject) или Custom Assertion / метод верификации, который называет проверяемое понятие.
  4. Проверяйте одно поведение на тест. Если один тест длинен потому, что прогоняет несколько исходов (нетерпеливый тест), разбейте его.
  5. Параметризуйте почти идентичные многословные тесты через test.each / it.each вместо копипаста.
// до — см. «Признаки и симптомы» (18 строк подготовки + 6 проверок, всего 32 строки)

// после
test('confirms the order and charges line-item total', () => {
  const customer = aCustomer();                          // Creation Method, значения по умолчанию
  const cart = aCartWith(aProduct({ price: 9.99 }), 2);  // только релевантный вход

  const order = orderService.placeOrder(customer, cart);

  expect(order).toMatchObject({                          // Expected Object, а не поле-за-полем
    status: 'CONFIRMED',
    total: 19.98,
  });
});

Сценарий теперь читается за секунды: покупатель берёт две единицы товара за $9.99 → заказ подтверждён с итогом $19.98. Хелперы (aCustomer, aProduct, aCartWith) — это общие, протестированные утилиты, поэтому шум живёт в одном месте, а не в каждом тесте.

Страховка: ограничьте длину тестовой функции и число проверок в линтере (см. детекторы), чтобы тесты не могли тихо разрастись обратно в многословные.

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

  • eslint max-lines-per-function — Ограничивает максимальное число строк в функции — помечает тела тестов, превышающие заданный лимит, ключевую меру многословного теста.
  • eslint max-statements — Ограничивает максимальное число операторов в блоке функции — лимитирует то, сколько может делать один тест (стрелочный/функциональный коллбэк).
  • sonar javascript:S138 — Функции не должны содержать слишком много строк кода — применяется к тестовым функциям; помечает раздутые, многозадачные тестовые методы (RSPEC-138).
  • eslint-jest jest/max-expects — Ограничивает максимальное число вызовов expect() на тест (по умолчанию 5) — ловит грань нагромождения проверок в многословных/жадных тестах.
  • eslint-vitest vitest/max-expects — Ограничивает максимальное число вызовов expect() на тест (по умолчанию 5) — эквивалент jest/max-expects для Vitest.