Тест с магическими числами.
Тест жёстко прописывает необъяснённые числовые литералы во входных данных и проверках, скрывая, что эти числа означают и откуда они взялись.
##Signs and Symptoms
Вы распознаёте тест с магическими числами, когда аргументы и проверки теста полны голых чисел, смысл и происхождение которых не очевидны из кода. Читателю приходится реконструировать (или просто доверять) то, почему ожидается конкретное значение.
Характерные признаки:
- Числовые литералы появляются прямо как аргументы проверок:
expect(result).toBe(54.13),assertEquals(86400, ttl). - Один и тот же литерал повторяется в подготовке, действии и ожидаемом значении, и ничто не связывает их именем.
- Числа кодируют доменные понятия, которые не прописаны явно (
3600= час,200= HTTP OK,0.0825= ставка налога). - Рядом с числом стоит комментарий, поясняющий его, — признак того, что само число следовало бы назвать.
- Во время ревью люди спрашивают «почему
42?» или «откуда берётся54.13?», и никто не может ответить, не перезапустив код.
// Запах: что такое 8.25? почему 54.13? что за 50 спрятано внутри хелпера?
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
Это специфическая, тестовая разновидность общего запаха магическое число (Magic Number) и классический вклад в запах непрозрачный тест (Obscure Test) (Месарош): читатель не может понять тест из самого теста.
##Reasons for the Problem
Почему это происходит
- Литерал — путь наименьшего сопротивления: вы вводите значение, увиденное в отладчике, или копируете фактический вывод неудачного прогона прямо в проверку, пока она не станет зелёной («угадай значение» / вставка вывода).
- У автора уже есть доменный контекст в голове, поэтому
3600или8.25кажутся самоочевидными в момент написания. - Значения фикстуры выбираются произвольно (
new User(25, ...)) лишь для того, чтобы удовлетворить конструктор, без раздумий о смысле.
Почему это вредно
- Читаемость / намерение. Число вроде
54.13констатирует факт, но не причину. Ревьюеры и будущие сопровождающие не могут понять, является ли это намеренным ожиданием, границей или случайностью. Тест перестаёт быть исполняемой документацией. - Сопровождаемость. Когда меняется правило (ставка налога, тайм-аут, размер страницы), вам приходится разыскивать каждую копию литерала и понимать, какая
7означала «дни», а какая — «максимум попыток». Безымянное дублирование делает безопасные правки дорогими и подверженными ошибкам. - Надёжность / ложная уверенность. Если ожидаемое значение неверно — или верно лишь по совпадению — ничто в тесте этого не выявит. Хуже того, авторы часто «чинят» тест с магическим числом, пересчитывая ожидаемое значение по боевой формуле (
expect(total).toBe(subtotal * (1 + rate)))), превращая проверку в тавтологию, которая переписывает тестируемый код и никогда не может упасть по правильной причине. - Диагностика. Когда такой тест ломается, сообщение об ошибке — это всего лишь «ожидалось 54.13, получено 54.12» без подсказки о том, какой вход или правило дали это число, что замедляет отладку.
##Treatment
Примените замену магического числа символьной константой (Replace Magic Number with Symbolic Constant, Месарош): дайте каждому значимому значению имя, объясняющее его роль, и сделайте связь между входами и ожидаемым результатом явной.
Конкретные шаги:
- Назовите входные данные. Вынесите литералы, используемые как тестовые данные, в локальные константы с понятными именами или в вызовы строителя фикстур (
const SUBTOTAL = 50.00,const TAX_RATE_PCT = 8.25). Используйте «Мать объектов» (Object Mother) / строитель для объектных фикстур, чтобы были видны только те значения, которые важны для теста. - Назовите и поясните ожидаемое значение. Оставьте ожидаемый результат независимым литералом, но дайте ему имя и задокументируйте, как он был выведен (
const EXPECTED_TOTAL = 54.13; // 50.00 + налог 8.25%). Не пересчитывайте его по боевой формуле — это лишь перепроверяет код против самого себя. - Свяжите входные данные с проверкой, чтобы читатель мог проверить арифметику на глаз, либо сверяйтесь с производным, но независимым эталонным значением.
- Оставьте по-настоящему самоочевидные значения как есть.
0,1,-1, индексы массивов и очевидные количества (items).toHaveLength(2)) обычно не нуждаются в именах; приберегите константы для значений, смысл которых не самоочевиден. Выносите общую константу в единый источник истины только тогда, когда это действительно одно и то же понятие везде.
// До
test('checkout works', () => {
const total = checkout(cartFor(50), 8.25);
expect(total).toBe(54.13);
});
// После
const SUBTOTAL = 50.00;
const TAX_RATE_PCT = 8.25;
const EXPECTED_TOTAL = 54.13; // SUBTOTAL плюс налог с продаж 8.25%
test('applies sales tax to the subtotal', () => {
const total = checkout(cartFor(SUBTOTAL), TAX_RATE_PCT);
expect(total).toBe(EXPECTED_TOTAL);
});
Теперь имена несут намерение; если правило налога изменится, правка будет локальной и очевидной, а ожидаемое значение останется честной независимой проверкой, а не тавтологией.
##Detected by
- eslint no-magic-numbers — Магические числа следует объявлять как именованные константы
- typescript-eslint @typescript-eslint/no-magic-numbers — Запретить магические числа (TypeScript)
- sonar javascript:S109 — Магические числа не должны использоваться
- checkstyle MagicNumber — MagicNumber
- tsDetect Magic Number Test — Magic Number Test (детектор, специфичный для тестов)