Многословный тест.
Тест, который использует гораздо больше кода, чем нужно для изложения своего сценария, погребая ту единственную связку «причина-следствие», которую он проверяет, под шаблонной подготовкой, нерелевантными данными и проверками поле-за-полем.
##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):
- Извлеките Creation Methods / используйте билдер. Замените inline-конструирование объектов выразительно названным хелпером, который поставляет разумные значения по умолчанию; передавайте только те значения, которые тесту действительно важны (Parameterized Creation Method). Это убивает и шаблонный код, и Irrelevant Information.
- Задавайте нерелевантные данные по умолчанию внутри хелпера, сигнализируя «эти значения не влияют на исход».
- Замените проверки поле-за-полем на Expected Object (
toEqual/toMatchObject) или Custom Assertion / метод верификации, который называет проверяемое понятие. - Проверяйте одно поведение на тест. Если один тест длинен потому, что прогоняет несколько исходов (Eager Test), разбейте его.
- Параметризуйте почти идентичные многословные тесты через
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.