Чек-лист тестирования денежных сумм и чисел: 35 пунктов, где всё ломается
Открой консоль и набери 0.1 + 0.2. Получишь 0.30000000000000004. Это не баг языка — это IEEE 754, двоичный float, в котором десятичные дроби не представляются точно. А теперь представь, что на этом считается баланс, скидка или налог. Деньги и числа выглядят как самая простая часть продукта — и именно поэтому в них живут самые дорогие баги: копейка, потерянная на округлении, сумма частей, не сходящаяся с целым, «1 234,56», распарсенная как 1. Разберём, что прогонять.
Деньги не хранят во float
Первое и главное: денежные суммы нельзя держать в float/double. Погрешность накапливается, и на тысячах операций «копейки» превращаются в расхождения. Правильно — целые в минимальных единицах (копейки, центы) или decimal-тип. Что проверить QA:
- Сумма считается в целых минимальных единицах, а не во float.
- Многократные операции (начисления/списания в цикле) не накапливают погрешность.
- Итог совпадает при разном порядке слагаемых (a+b+c = c+b+a).
Округление — где и как
Округление — источник «потерянной копейки». Договоритесь и проверьте:
- Где округляем: на каждой операции или только при отображении? Разные ответы дают разный итог.
- Как округляем: half-up, half-even (bankers’), вниз? Для денег обычно фиксируют правило — проверь, что оно одно везде.
- Split суммы: 100 ₽ разделить на 3 → 33.33 + 33.33 + 33.34; сумма частей ОБЯЗАНА равняться целому, без потерянной/лишней копейки.
- Проценты: скидка 33% от 10 ₽, налог, комиссия — округление не должно давать бесплатный товар или отрицательную сумму.
Форматирование по локали
Одно и то же число в разных локалях выглядит по-разному, и это ломает и вывод, и ввод:
- Разделители:
1,234.56(US) vs1 234,56(RU/FR) vs1.234,56(DE). Проверь и отображение, и парсинг ввода. - Позиция символа валюты:
$1,234.56,1 234,56 ₽,1.234,56 €— до/после, с пробелом или без. - Отрицательные:
-100,(100),100-, красным — по локали и по требованию. - RTL (арабский/иврит): число и знак валюты не разъезжаются.
- Группировка: индийская система (1,00,000) отличается от западной (100,000).
Валюты — не все одинаковые
- Число знаков после запятой разное: JPY — 0 (¥100, не ¥100.00), USD/EUR — 2, BHD/KWD — 3. Хардкод «2 знака» = баг для половины валют.
- Символ vs код:
$неоднозначен (US/CA/AU); в отчётах и мультивалюте безопаснее ISO-код (USD, EUR). - Конвертация: курс, округление после конвертации, устаревший курс, конвертация туда-обратно не должна «съедать» деньги.
Граничные значения
- Ноль:
0.00, бесплатно, «—» вместо нуля где уместно. - Отрицательные: возвраты, долги, корректировки — допустимы ли, как отображаются.
- Очень большие: переполнение типа, обрезка в UI, разделители на миллионах/миллиардах.
- Очень маленькие:
0.001, суммы меньше минимальной единицы — округление, а не «пропажа». - null / пусто: не «NaN», не «undefined», не
0вместо «нет данных».
Ввод пользователя
Поле суммы — источник мусора. Проверь:
- Запятая vs точка как десятичный разделитель (юзер вводит по своей привычке).
- Пробелы и группировка при вставке: копипаст
1 000,00/1,000.00должен распарситься, а не стать1. - Лишние нули (
007,1.500— это 1.5 или 1500?), ведущие/хвостовые. - Буквы, знаки, эмодзи, минус в середине — отсекаются или чинятся понятно.
- Больше двух знаков после запятой в поле валюты с 2 знаками — что происходит.
Бизнес-логика: скидки, налоги, комиссии
- Скидка + налог + комиссия в одной корзине — порядок применения влияет на итог, зафиксирован ли он.
- Скидка не уводит цену ниже нуля; налог считается от базы или от суммы со скидкой — по требованию.
- Итог корзины = сумме позиций после всех округлений (частая рассинхронизация «строки vs итог»).
- Купон/промокод на грани (скидка больше суммы) не даёт отрицательный чек.
Чек-лист (быстрый прогон)
- Деньги — в целых минимальных единицах или decimal, не во float.
- Многократные операции не накапливают погрешность; порядок слагаемых не влияет.
- Правило округления одно везде; зафиксировано, где округляем.
- Split суммы: сумма частей = целому, без потерянной копейки.
- Разделители тысяч/дробной по локали — и вывод, и парсинг ввода.
- Позиция и формат символа валюты по локали; RTL не разъезжается.
- Число знаков по валюте (JPY 0, USD 2, KWD 3), не хардкод «2».
- Символ vs ISO-код там, где возможна неоднозначность/мультивалюта.
- Конвертация: курс, округление, туда-обратно без потерь.
- Границы: 0, отрицательные, огромные (переполнение), крошечные, null.
- Ввод: запятая/точка, пробелы, копипаст, лишние нули, буквы.
- Скидки/налоги/комиссии: порядок, не ниже нуля, итог = сумме позиций.
Коротко — что забрать с собой
0.1 + 0.2 ≠ 0.3: деньги не во float — целые минимальные единицы или decimal.- Округление — главный источник «потерянной копейки»; фиксируй правило и проверяй split.
- Форматирование и парсинг зависят от локали: разделители, символ валюты, RTL.
- Валюты имеют разное число знаков — «2» хардкодом это баг.
- Тестируй границы (0, минус, переполнение, крошечные) и грязный ввод.
Почитать: 0.30000000000000004.com — про float · Martin Fowler — Money pattern · MDN — Intl.NumberFormat · ISO 4217 — коды и знаки валют