checklistmoneynumberslocalizationqa

Чек-лист тестирования денежных сумм и чисел: 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) vs 1 234,56 (RU/FR) vs 1.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 — коды и знаки валют