ConstructiCat Logo
CodeBust.
Browse section ▾

Дублирование тестового кода.

Дублирование тестового кода — это когда один и тот же код подготовки, действия или проверки копируется в множество тестов, так что одно изменение вынуждает к правкам во многих местах, а тесты деградируют в хрупкие, почти идентичные копии.

##Signs and Symptoms

Вы распознаёте этот запах, когда тесты выглядят написанными копипастом, а не переиспользованием:

  • Одна и та же конструкция фикстуры/объекта дословно пересобирается в начале теста за тестом.
  • Идентичные последовательности проверок (одни и те же 3–4 вызова expect в том же порядке) повторяются между тестами.
  • Новые тесты очевидно склонированы из старого с подправленной одной строкой.
  • Одни и те же магические литералы (ID, URL-адреса, даты, строки ошибок) встречаются снова и снова.
  • Изменение одного конструктора или сигнатуры API ломает десятки тестов разом (хирургия дробью, shotgun surgery).
  • Вы видите почти дублированные тесты, отличающиеся только входными/ожидаемыми значениями, — явный кандидат на параметризованный/табличный тест.
test('flight can be cancelled', () => {
  const airport = new Airport('YYC', 'Calgary');                              // дублируется
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // дублируется
  flight.cancel();
  expect(flight.status).toBe('CANCELLED');
});

test('flight can be delayed', () => {
  const airport = new Airport('YYC', 'Calgary');                              // копипаст
  const flight  = new Flight('AC123', airport, new Date('2026-06-01T10:00')); // копипаст
  flight.delay(30);
  expect(flight.status).toBe('DELAYED');
});

##Reasons for the Problem

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

  • Копирование предыдущего теста — самый быстрый способ написать следующий.
  • К тестам относятся как к «гражданам второго сорта» — их не рефакторят и не держат к тому же стандарту DRY, что и боевой код.
  • Нет общих фикстур, методов создания (Creation Methods) или строителей тестовых данных, а авторы незнакомы с параметризованными/табличными тестами.

Почему это вредно

  • Сопровождаемость: Месарош напрямую связывает этот запах с хрупким тестом (Fragile Test) — когда «одни и те же последовательности кода появляются много раз во многих тестах», одно изменение в боевом коде означает правку одного и того же в N местах. Стоимость сопровождения растёт с числом копий, а не с числом различных поведений.
  • Читаемость: повторяющийся шаблонный код хоронит ту единственную строку, которая на самом деле делает каждый тест особенным, поэтому читатели не могут быстро увидеть, что именно проверяется.
  • Надёжность: копипаст провоцирует ошибки копипаста, а исправления применяются к одной копии, но не к её собратьям, оставляя несогласованные, противоречивые тесты.
  • Ложная уверенность: ошибочная проверка, которую размножили, теперь неверна сразу во многих местах, а клонированные тесты молча расходятся, пока больше не проверяют того, что заявляют их имена.

Оговорка — DRY против DAMP: тесты также ценят свойство быть описательными и осмысленными фразами (Descriptive And Meaningful Phrases). Не абстрагируйте чрезмерно до того, что читателю придётся гоняться за хелперами, чтобы понять тест. Выносите подлинное, раскрывающее намерение дублирование; сохраняйте существенную, специфичную для каждого теста деталь видимой и локальной.

##Treatment

Устраните случайное дублирование, сохраняя суть каждого теста очевидной:

  1. Вынесите тестовые утилиты / методы создания (Object Mother, строитель тестовых данных) для повторяющейся конструкции объектов, чтобы каждый тест называл только важные для него значения.
  2. Используйте beforeEach / неявную подготовку (Implicit Setup) для контекста, который действительно общий и релевантен каждому тесту в блоке, — но избегайте сокрытия состояния, от которого зависит тест (это меняет дублирование на непрозрачный тест (Obscure Test)).
  3. Вынесите пользовательские проверки / хелперы верификации для повторяющихся многошаговых последовательностей проверок, идеально проверяющих одно логическое условие.
  4. Сверните почти идентичные тесты в параметризованные / табличные тесты (it.each / test.each), чтобы пары вход + ожидаемый вывод жили в одной таблице.
  5. Замените дублированные магические литералы именованными константами или значениями по умолчанию строителя.

До — дублированная конструкция и три почти идентичных теста:

test('rejects negative amount', () => {
  expect(() => validateAmount(-1)).toThrow(RangeError);
});
test('rejects zero amount', () => {
  expect(() => validateAmount(0)).toThrow(RangeError);
});
test('rejects NaN amount', () => {
  expect(() => validateAmount(NaN)).toThrow(RangeError);
});

После — метод создания убирает дублирование подготовки, а таблица заменяет клоны:

// общий метод создания: тесты указывают только значимые переопределения
const aFlight = (overrides = {}) =>
  new Flight('AC123', new Airport('YYC', 'Calgary'),
             new Date('2026-06-01T10:00'), overrides);

it.each([-1, 0, NaN])('rejects invalid amount %p', (amount) => {
  expect(() => validateAmount(amount)).toThrow(RangeError);
});

Прогоните детектор копипаста (ниже) по вашим тестовым исходникам, затем рефакторите сначала самые крупные/наиболее повторяющиеся блоки.

##Detected by

  • eslint-sonarjs no-identical-functionsФункции не должны иметь идентичных реализаций
  • eslint-sonarjs no-duplicate-stringСтроковые литералы не должны дублироваться
  • sonar javascript:S4144Функции не должны иметь идентичных реализаций
  • pmd cpdДетектор копипаста (CPD) — дублированные блоки кода