Дублирующаяся проверка.
Один тестовый метод проверяет одно и то же условие несколько раз — повторяя идентичное утверждение или заново проверяя эквивалентную логику — вместо того чтобы удалить избыточную проверку или разнести разные случаи по отдельным сфокусированным тестам.
##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
- Удалите точные дубликаты. Если проверка байт в байт совпадает с другой в том же тесте, уберите её — это балласт, а не покрытие.
- Параметризуйте эквивалентные случаи. Когда "дубликаты" на самом деле представляют одно и то же условие с разными входными данными, преобразуйте их в табличный/параметризованный тест (
test.eachв Jest/Vitest,@ParameterizedTestв JUnit 5). Каждая строка отображается в отчёте и именуется отдельно, поэтому падения точно указывают на проблемный вход. - Разнесите по-настоящему разные условия по отдельным тестам, каждый с именем, которое сообщает, что именно он проверяет (
принимает имена хостов с дефисами,отклоняет теги script). - Именуйте по намерению. Имя теста должно описывать одно поведение; если это не получается — это сигнал к разделению.
// До — продублированные / собранные вместе проверки в одном непрозрачном тесте
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);
});
Дублирующая строка удалена, разные входы явны, а падение называет точный случай.