ConstructiCat Logo
CodeBust.
Browse section ▾

Дублирующаяся проверка.

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

##Signs and Symptoms

Вы видите, как одна и та же проверка встречается дважды (тот же матчер, те же аргументы) в одном тесте, либо несколько скопированных блоков проверок, которые отличаются лишь литеральными значениями и заново проверяют одно и то же поведение. Характерные признаки:

  • Буквально идентичная строка проверки встречается два и более раз в теле теста.
  • Множество вызовов expect(...)/assertEquals(...), проверяющих одно и то же условие с разными входными данными, втиснуты в один метод, имя которого описывает лишь один сценарий.
  • Оставшиеся от отладки проверки ("дай-ка я ещё проверю X"), которые так и не убрали.
  • Имя теста (например, testXmlSanitizer) не даёт никакой подсказки, какая из множества его проверок упала.
test('sanitizer accepts valid input', () => {
  expect(isValid('plain text')).toBe(true);
  expect(isValid('with spaces')).toBe(true);
  expect(isValid('Fritz-box')).toBe(true);   // "минус допустим"
  expect(isValid('Fritz-box')).toBe(true);   // <-- точный дубликат, ничего не добавляет
  expect(isValid('<script>')).toBe(false);
});

Продублированная строка Fritz-box — это канонический Duplicate Assert; более широкий запах в том, что один метод незаметно собирает множество проверок одного и того же условия под одним именем.

##Reasons for the Problem

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

  • Копипаст. Блок проверки копируется, и в нём меняются литералы (а иногда не меняется вообще ничего).
  • Остатки отладки. Дополнительные проверки, добавленные для исследования поведения, забывают убрать.
  • Группировка. Разработчики тестируют "один метод", сваливая все случаи в один тест вместо того чтобы разделить их, и получают повторяющиеся, почти идентичные проверки.

Чем это вредит

  • Ложная уверенность. По-настоящему идентичная, продублированная проверка добавляет ноль покрытия — она никогда не упадёт, если проходит её двойник, — зато создаёт впечатление, что тест тщательнее, чем он есть.
  • Трудная диагностика падений. Когда несколько однотипных проверок собраны в одном методе, отчёт о падении указывает на метод, а не на конкретные входные данные, которые сломались. Хуже того, при стандартной конфигурации тест останавливается на первой упавшей проверке, поэтому последующие продублированные проверки не выполняются вовсе — вы чините одну, перезапускаете, натыкаетесь на следующую (пересекается с Assertion Roulette).
  • Плохая читаемость. Читатель не может понять, намеренно ли это повторение (разные случаи) или ошибка (настоящий дубликат), а имя теста документирует лишь одно из множества условий.
  • Стоимость сопровождения. Измените тестируемое поведение — и вам придётся выискивать и обновлять каждую продублированную проверку; пропустите одну — и набор тестов станет противоречивым.

##Treatment

  1. Удалите точные дубликаты. Если проверка байт в байт совпадает с другой в том же тесте, уберите её — это балласт, а не покрытие.
  2. Параметризуйте эквивалентные случаи. Когда "дубликаты" на самом деле представляют одно и то же условие с разными входными данными, преобразуйте их в табличный/параметризованный тест (test.each в Jest/Vitest, @ParameterizedTest в JUnit 5). Каждая строка отображается в отчёте и именуется отдельно, поэтому падения точно указывают на проблемный вход.
  3. Разнесите по-настоящему разные условия по отдельным тестам, каждый с именем, которое сообщает, что именно он проверяет (принимает имена хостов с дефисами, отклоняет теги script).
  4. Именуйте по намерению. Имя теста должно описывать одно поведение; если это не получается — это сигнал к разделению.
// До — продублированные / собранные вместе проверки в одном непрозрачном тесте
test('isValid', () => {
  expect(isValid('plain text')).toBe(true);
  expect(isValid('with spaces')).toBe(true);
  expect(isValid('Fritz-box')).toBe(true);
  expect(isValid('Fritz-box')).toBe(true); // дубликат
  expect(isValid('<script>')).toBe(false);
});

// После — одна проверка, каждый случай назван и отображается в отчёте отдельно
test.each([
  ['plain text', true],
  ['with spaces', true],
  ['Fritz-box',  true],   // минус допустим
  ['<script>',   false],  // отклоняет разметку
])('isValid(%j) === %s', (input, expected) => {
  expect(isValid(input)).toBe(expected);
});

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