ConstructiCat Logo
CodeBust.
Browse section ▾

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

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

##Signs and Symptoms

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

  • Тестовый метод длинный (каталог 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 = 'ada@example.com';
  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);
});

Около 25 строк конструирования охраняют двухстрочную проверку итогов и статуса.

##Reasons for the Problem

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

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

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

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

##Treatment

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

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

// после
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) — это общие, протестированные утилиты, поэтому шум живёт в одном месте, а не в каждом тесте.

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

##Detected by

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