ConstructiCat Logo
CodeBust.
Browse section ▾

Тест с магическими числами.

Тест жёстко прописывает необъяснённые числовые литералы во входных данных и проверках, скрывая, что эти числа означают и откуда они взялись.

##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, Месарош): дайте каждому значимому значению имя, объясняющее его роль, и сделайте связь между входами и ожидаемым результатом явной.

Конкретные шаги:

  1. Назовите входные данные. Вынесите литералы, используемые как тестовые данные, в локальные константы с понятными именами или в вызовы строителя фикстур (const SUBTOTAL = 50.00, const TAX_RATE_PCT = 8.25). Используйте «Мать объектов» (Object Mother) / строитель для объектных фикстур, чтобы были видны только те значения, которые важны для теста.
  2. Назовите и поясните ожидаемое значение. Оставьте ожидаемый результат независимым литералом, но дайте ему имя и задокументируйте, как он был выведен (const EXPECTED_TOTAL = 54.13; // 50.00 + налог 8.25%). Не пересчитывайте его по боевой формуле — это лишь перепроверяет код против самого себя.
  3. Свяжите входные данные с проверкой, чтобы читатель мог проверить арифметику на глаз, либо сверяйтесь с производным, но независимым эталонным значением.
  4. Оставьте по-настоящему самоочевидные значения как есть. 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 MagicNumberMagicNumber
  • tsDetect Magic Number TestMagic Number Test (детектор, специфичный для тестов)