Testing Money and Numbers: A 35-Point Checklist of Where It All Breaks
Open a console and type 0.1 + 0.2. You’ll get 0.30000000000000004. That’s not a language bug — it’s IEEE 754, binary float, where decimal fractions can’t be represented exactly. Now imagine a balance, a discount, or a tax being computed on that. Money and numbers look like the simplest part of a product — which is exactly why the most expensive bugs live there: a cent lost to rounding, the sum of parts that doesn’t match the whole, “1,234.56” parsed as 1. Here’s what to run.
Money isn’t stored in float
First and foremost: monetary amounts must not live in float/double. Error accumulates, and across thousands of operations “cents” turn into discrepancies. The right way — integers in minor units (cents) or a decimal type. What QA checks:
- The amount is computed in integer minor units, not float.
- Repeated operations (credits/debits in a loop) don’t accumulate error.
- The result matches regardless of addend order (a+b+c = c+b+a).
Rounding — where and how
Rounding is the source of the “lost cent.” Agree on it and verify:
- Where do we round: on every operation or only on display? Different answers give different totals.
- How do we round: half-up, half-even (banker’s), down? For money there’s usually a fixed rule — check it’s the same everywhere.
- Splitting an amount: $100 split by 3 → 33.33 + 33.33 + 33.34; the sum of parts MUST equal the whole, with no lost or extra cent.
- Percentages: 33% off $10, tax, a fee — rounding must not produce a free item or a negative amount.
Locale formatting
The same number looks different across locales, and that breaks both output and input:
- Separators:
1,234.56(US) vs1 234,56(RU/FR) vs1.234,56(DE). Check both display and input parsing. - Currency symbol position:
$1,234.56,1 234,56 ₽,1.234,56 €— before/after, with or without a space. - Negatives:
-100,(100),100-, red — by locale and by requirement. - RTL (Arabic/Hebrew): the number and currency sign don’t come apart.
- Grouping: the Indian system (1,00,000) differs from the Western one (100,000).
Currencies aren’t all the same
- Decimal places differ: JPY — 0 (¥100, not ¥100.00), USD/EUR — 2, BHD/KWD — 3. Hardcoding “2 places” is a bug for half the currencies.
- Symbol vs code:
$is ambiguous (US/CA/AU); in reports and multi-currency, the ISO code (USD, EUR) is safer. - Conversion: rate, rounding after conversion, stale rate, round-tripping must not “eat” money.
Boundary values
- Zero:
0.00, free, ”—” instead of zero where appropriate. - Negatives: refunds, debts, adjustments — are they allowed, how are they shown.
- Very large: type overflow, UI truncation, separators on millions/billions.
- Very small:
0.001, amounts below the minor unit — rounded, not “vanished.” - null / empty: not “NaN,” not “undefined,” not
0instead of “no data.”
User input
An amount field is a source of garbage. Check:
- Comma vs dot as the decimal separator (the user types by their own habit).
- Spaces and grouping on paste: pasting
1 000,00/1,000.00must parse, not become1. - Extra zeros (
007,1.500— is that 1.5 or 1500?), leading/trailing. - Letters, symbols, emoji, a minus in the middle — rejected or fixed clearly.
- More than two decimals in a 2-decimal currency field — what happens.
Business logic: discounts, taxes, fees
- Discount + tax + fee in one cart — the order of application affects the total; is it fixed?
- A discount doesn’t push the price below zero; tax is computed from the base or from the discounted amount — per requirement.
- The cart total = the sum of line items after all rounding (the frequent “lines vs total” desync).
- A coupon/promo at the edge (discount larger than the amount) doesn’t produce a negative receipt.
Checklist (quick pass)
- Money in integer minor units or decimal, not float.
- Repeated operations don’t accumulate error; addend order doesn’t matter.
- One rounding rule everywhere; where we round is fixed.
- Splitting: sum of parts = the whole, no lost cent.
- Thousands/decimal separators by locale — both output and input parsing.
- Currency symbol position and format by locale; RTL doesn’t break.
- Decimal places by currency (JPY 0, USD 2, KWD 3), not a hardcoded “2.”
- Symbol vs ISO code where ambiguity/multi-currency is possible.
- Conversion: rate, rounding, round-trip without loss.
- Boundaries: 0, negatives, huge (overflow), tiny, null.
- Input: comma/dot, spaces, paste, extra zeros, letters.
- Discounts/taxes/fees: order, not below zero, total = sum of items.
In short — what to take with you
0.1 + 0.2 ≠ 0.3: money isn’t in float — integer minor units or decimal.- Rounding is the main source of the “lost cent”; fix the rule and test splitting.
- Formatting and parsing depend on locale: separators, currency symbol, RTL.
- Currencies have different decimal places — a hardcoded “2” is a bug.
- Test boundaries (0, negative, overflow, tiny) and dirty input.
Further reading: 0.30000000000000004.com — on float · Martin Fowler — Money pattern · MDN — Intl.NumberFormat · ISO 4217 — currency codes and symbols