ConstructiCat Logo
CodeBust.
Browse section ▾

Универсальная фикстура.

Общая подготовка строит одну большую фикстуру «на все случаи жизни», покрывающую потребности всех тестов, но каждый отдельный тест задействует лишь небольшую её часть.

##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

Стремитесь к минимальной фикстуре: каждый тест готовит только то, что нужно именно ему, и ничего больше.

  1. Проведите инвентаризацию использования. Для каждого общего поля выпишите, какие тесты его действительно читают. Всё, что используется лишь подмножеством, — кандидат на вынос из общей подготовки.
  2. Перенесите конструирование в создающие методы / билдеры тестовых данных (делегированная подготовка). Сохраняйте лаконичность тестов, не навязывая одну гигантскую фикстуру, — каждый тест вызывает билдер и переопределяет только важные ему атрибуты.
  3. Предпочитайте свежую фикстуру на каждый тест долгоживущей общей, устраняя межтестовую связанность и изменяемое общее состояние.
  4. Разделяйте по сценариям. Если группы тестов действительно делят небольшую фикстуру, разнесите их по узким блокам describe (или тестовым классам), каждый со своей минимальной подготовкой.
  5. Оставляйте в 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);
});

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