Избыточная проверка.
Избыточная проверка сравнивает значение с самим собой или с литералом, равным ему по построению, поэтому её исход предопределён, и она в принципе не может ни упасть, ни обнаружить регрессию.
##Signs and Symptoms
Проверка избыточна, когда её результат предрешён ещё до запуска тестируемого кода: ожидаемый и фактический операнды — это одно и то же значение, либо оба являются литералами, заведомо равными (или неравными). Каталог xUnit/Test Smells (Peruma и др.) определяет её как «тестовый метод, содержащий инструкцию проверки, в которой ожидаемый и фактический параметры совпадают», и отмечает, что такая проверка поэтому «либо всегда истинна, либо всегда ложна».
Как её распознать:
assertEquals/toBe/toEqual, у которого оба аргумента — это одно и то же выражение или переменная.- Булев литерал, проверяемый против самого себя, например
assertTrue(true)илиexpect(true).toBe(true). - Сравнение, константность которого может доказать компилятор/линтер, вроде
expect(x === x).toBe(true). - «Проверка на вменяемость», которая повторяет только что объявленную константу вместо того, чтобы задействовать систему.
// Запах: исход предопределён, тестируемый код вообще не задействован
test('user is active', () => {
expect(true).toBe(true); // всегда проходит
const status = 'active';
expect(status).toBe('active'); // повторяет литерал, ничего не доказывает
expect(user.id).toEqual(user.id); // значение сравнивается с самим собой
});
Надёжный признак: можно полностью удалить продакшен-код, а проверка всё равно останется зелёной.
##Reasons for the Problem
Почему это происходит
- Забытая отладка. Каталог прямо отмечает, что этот запах «вносится разработчиками в отладочных целях, а затем забывается», — жёстко зашитая заглушка
assertTrue(true), доживающая до коммита. - Дрейф из-за копипаста / рефакторинга. В обе стороны
assertEqualsподставлена одна и та же переменная, или тестируемое значение переименовали так, что ожидаемое и фактическое схлопнулись в один и тот же символ. - Тавтология по построению. Значение проверяют против того же литерала, из которого его только что присвоили, вместо независимо выведенного ожидания.
- Театр покрытия. Проверку добавляют только ради соблюдения правила «в каждом тесте должна быть проверка», ничего осмысленного при этом не проверяя.
Чем это вредит
- Ложная уверенность. Тест навсегда зелёный и учитывается в размере набора и покрытии, но не проверяет ничего. Он не способен поймать регрессию и потому маскирует прорехи в страховочной сетке.
- Надёжность бессмысленна. Тест, который никогда не может упасть, даёт нулевой сигнал; тот, что всегда ложен, — мёртвый груз, который игнорируют или ставят
skip. - Читаемость. Читатели зря тратят силы, восстанавливая предполагаемое поведение по проверке, утверждающей тавтологию; тест больше не документирует требование.
- Сопровождаемость. Избыточные проверки накапливают шум, раздувают метрики и подрывают доверие к набору тестов, побуждая людей перестать внимательно читать проверки.
##Treatment
Замените тавтологию проверкой, которая связывает независимо известное ожидаемое значение с фактическим результатом, выданным тестируемой системой.
Шаги:
- Найдите проверку с предопределённым исходом — один и тот же операнд с обеих сторон или два равных литерала.
- Определите настоящее намерение. Какое поведение этот тест должен был проверять? Если никакое, то проверка (или весь тест) мертва и подлежит удалению.
- Проверяйте выход SUT, а не вход. Подайте системе реальный вход и сравните вычисленный ею результат с жёстко зашитым, вычисленным вручную ожидаемым значением — а не с одним из её собственных входов/переменных.
- Держите ожидаемое и фактическое различными. Убедитесь, что ожидаемый операнд — это намеренно написанная вами константа, а фактический операнд — возвращаемое значение тестируемого кода (заодно это устраняет родственный запах неверного порядка аргументов).
- Перезапустите со сломанной реализацией (внесите мутацию), чтобы убедиться, что проверка действительно может упасть.
// До — избыточно: исход предопределён
test('discount', () => {
const total = 100;
expect(total).toBe(100); // повторяет литерал
expect(applyDiscount).toBe(applyDiscount); // значение против самого себя
});
// После — осмысленно: известный вход -> независимо ожидаемый выход
test('applies a 10% discount', () => {
expect(applyDiscount(100, 0.1)).toBe(90); // результат SUT против вручную вычисленного ожидания
});
Если заглушка вроде assertTrue(true) осталась от отладки — удалите её; если она замещала реальную проверку — напишите эту проверку.
##Detected by
- sonar javascript:S5863 — Проверкам не следует передавать один и тот же аргумент дважды
- sonar java:S5863 — Проверкам не следует передавать один и тот же аргумент дважды
- eslint no-constant-binary-expression — Запрет выражений, в которых операция не влияет на значение (помечает самосравнения / всегда истинные проверки вроде assert(a === a))