Логотип ConstructiCat
CodeBust.
Навигация по разделу ▾

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

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

##Признаки и симптомы

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

  • Одна и та же конструкция фикстуры/объекта дословно пересобирается в начале теста за тестом.
  • Идентичные последовательности проверок (одни и те же 3–4 вызова expect в том же порядке) повторяются между тестами.
  • Новые тесты очевидно склонированы из старого с подправленной одной строкой.
  • Одни и те же магические литералы (ID, URL-адреса, даты, строки ошибок) встречаются снова и снова.
  • Изменение одного конструктора или сигнатуры API ломает десятки тестов разом («Стрельба дробью»).
  • Вы видите почти дублированные тесты, отличающиеся только входными/ожидаемыми значениями, — явный кандидат на параметризованный/табличный тест.
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');
});

##Причины проблемы

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

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

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

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

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

##Лечение

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

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

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

// одна и та же конструкция в каждом тесте
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');
});

// …и клоны, отличающиеся только входными данными
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);
});

После — тесты вызывают метод создания aFlight(), а таблица заменяет клоны:

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

test('flight can be cancelled', () => {
  const flight = aFlight();
  flight.cancel();
  expect(flight.status).toBe('CANCELLED');
});

test('flight can be delayed', () => {
  const flight = aFlight();
  flight.delay(30);
  expect(flight.status).toBe('DELAYED');
});

// параметризованный тест заменяет клоны
it.each([-1, 0, NaN])('rejects invalid amount %p', (amount) => {
  expect(() => validateAmount(amount)).toThrow(RangeError);
});

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

##Обнаружение анализаторами

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