Универсальная фикстура.
Общая подготовка строит одну большую фикстуру «на все случаи жизни», покрывающую потребности всех тестов, но каждый отдельный тест задействует лишь небольшую её часть.
##Signs and Symptoms
Единственный beforeEach/setUp (или общая подготовка на уровне модуля) конструирует кучу объектов, засевает много записей и поднимает несколько моков — а любой отдельный тест трогает лишь один-два из них. Эвристика обнаружения из каталога тестовых запахов проста: не все поля, создаваемые в подготовке, используются всеми тестовыми методами.
Как это распознать:
- Блок подготовки длинный и разрастается с каждым новым тестом.
- Чтобы понять один тест, приходится прокручивать вверх и реверс-инжинирить, какие части фикстуры ему на самом деле важны.
- Многие тесты падают или требуют правок, когда вы меняете один общий объект, до которого большинству из них и дела нет.
- Одна и та же фикстура из одного места обслуживает совершенно разные сценарии (создание, валидация, оплата, права доступа).
// Запах: одна фикстура на всё; каждый тест использует от неё кусочек
let user, admin, product, cart, order, paymentGateway, shippingZone;
beforeEach(() => {
user = createUser({ name: 'Ada' });
admin = createAdmin();
product = createProduct({ price: 10 });
cart = createCart(user, [product]);
order = createOrder(cart);
paymentGateway = mockGateway();
shippingZone = createZone('EU');
});
test('cart subtotal sums its line items', () => {
// нужны только cart + product, но платит за admin, order, gateway, zone…
expect(cart.subtotal()).toBe(10);
});
##Reasons for the Problem
Почему это происходит
- Слишком агрессивно применённый DRY. Подготовку сводят в один общий хук, чтобы «избежать дублирования», и потребности каждого нового теста прикручивают к той же фикстуре.
- Органический рост. Фикстура обрастает объектами по мере добавления тестов, и никто не вычищает то, что старые тесты больше не используют.
- Инерция неявной подготовки. Когда большой
beforeEachуже есть, добавить ещё одну строку проще, чем создавать узкоспециализированную фикстуру для нового случая.
Чем это вредит
- Читаемость / тесты-как-документация. Тест должен читаться как ясная связь причина→следствие. Когда большая часть фикстуры — несущественный шум, читатель не может понять, что на самом деле определяет результат. Противоядие Месароша, минимальная фикстура, существует именно потому, что тест с наименьшей фикстурой всегда легче понять.
- Сопровождаемость. Изменение, которого требует один тест (например, дать
productновое обязательное поле), вынуждает править подготовку, общую для всех тестов, порождая каскадные поломки и связывая друг с другом не связанные тесты. - Надёжность. Общее, изменяемое состояние фикстуры позволяет тестам влиять друг на друга и затрудняет локализацию падений — классический источник зависящего от порядка флакования.
- Скорость. Каждый тест оплачивает полную стоимость построения всей фикстуры. Универсальная фикстура числится среди причин медленных тестов.
- Ложная уверенность. Тесты оказываются переспецифицированы под случайные детали фикстуры, поэтому проходят или падают по причинам, не связанным с проверяемым поведением.
##Treatment
Стремитесь к минимальной фикстуре: каждый тест готовит только то, что нужно именно ему, и ничего больше.
- Проведите инвентаризацию использования. Для каждого общего поля выпишите, какие тесты его действительно читают. Всё, что используется лишь подмножеством, — кандидат на вынос из общей подготовки.
- Перенесите конструирование в создающие методы / билдеры тестовых данных (делегированная подготовка). Сохраняйте лаконичность тестов, не навязывая одну гигантскую фикстуру, — каждый тест вызывает билдер и переопределяет только важные ему атрибуты.
- Предпочитайте свежую фикстуру на каждый тест долгоживущей общей, устраняя межтестовую связанность и изменяемое общее состояние.
- Разделяйте по сценариям. Если группы тестов действительно делят небольшую фикстуру, разнесите их по узким блокам
describe(или тестовым классам), каждый со своей минимальной подготовкой. - Оставляйте в
beforeEachтолько то, что действительно общее и небольшое; остальное переносите прямо в тест, чтобы причина и следствие были рядом с проверкой.
// До: универсальная фикстура в общем хуке (см. «Признаки и симптомы»)
// После: минимальная, делегированная подготовка — каждый тест строит только нужное
test('cart subtotal sums its line items', () => {
const cart = aCart().withItem(aProduct().price(10)).build();
expect(cart.subtotal()).toBe(10);
});
test('checkout charges the payment gateway', () => {
const gateway = mockGateway();
const cart = aCart().withItem(aProduct().price(10)).build();
checkout(cart, gateway);
expect(gateway.charge).toHaveBeenCalledWith(10);
});
Теперь каждый тест читается сверху вниз как самодостаточная история, билдеры впитывают шаблонный код, и изменение одного сценария больше не задевает остальные.