Дублирование тестового кода.
Дублирование тестового кода — это когда один и тот же код подготовки, действия или проверки копируется в множество тестов, так что одно изменение вынуждает к правкам во многих местах, а тесты деградируют в хрупкие, почти идентичные копии.
##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
Устраните случайное дублирование, сохраняя суть каждого теста очевидной:
- Вынесите тестовые утилиты / методы создания (Object Mother, строитель тестовых данных) для повторяющейся конструкции объектов, чтобы каждый тест называл только важные для него значения.
- Используйте
beforeEach/ неявную подготовку (Implicit Setup) для контекста, который действительно общий и релевантен каждому тесту в блоке, — но избегайте сокрытия состояния, от которого зависит тест (это меняет дублирование на непрозрачный тест (Obscure Test)). - Вынесите пользовательские проверки / хелперы верификации для повторяющихся многошаговых последовательностей проверок, идеально проверяющих одно логическое условие.
- Сверните почти идентичные тесты в параметризованные / табличные тесты (
it.each/test.each), чтобы пары вход + ожидаемый вывод жили в одной таблице. - Замените дублированные магические литералы именованными константами или значениями по умолчанию строителя.
До — дублированная конструкция и три почти идентичных теста:
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) — дублированные блоки кода