skip to content

Why does JUnit 5 offer assertEquals overloads for double and float that take a third 'delta' argument, and how do you decide what delta to pass?

level: middleimportance: must knowfreq 55%

answer

  1. IEEE 754: 0.1 has no exact binary form
  2. 0.1 + 0.2 = 0.30000000000000004
  3. pass when |expected - actual| <= delta
  4. no-delta overload = bitwise: NaN==NaN, +0.0 != -0.0
  5. big magnitudes -> relative tolerance; money -> BigDecimal

basics

~20 s

Binary floating point cannot represent most decimals exactly, so arithmetic results drift by tiny amounts — 0.1 + 0.2 is not 0.3. The delta is the tolerance: the assertion passes when the absolute difference is at most delta. Pick it from the magnitudes and the number of operations, not by habit.

solid answer

~50 s

`double` and `float` are binary fractions, so decimal values like 0.1 have no exact representation and every operation rounds. `0.1 + 0.2` yields `0.30000000000000004`, so an exact comparison fails even though the computation is correct. The delta overload — `assertEquals(expected, actual, delta)` — passes when `Math.abs(expected - actual) <= delta`. Choosing delta: think about the magnitude of the numbers and how much rounding accumulated. For a couple of operations on values near 1, `1e-9` is generous. For large values, an absolute tolerance is wrong — use a relative one, `Math.abs(expected) * 1e-9`. For money, do not use floating point at all: use `BigDecimal` or long minor units and assert exactly. And do not paper over a real bug with a fat delta like `0.5`. JUnit also has a no-delta `assertEquals(double, double)` overload that compares bit patterns, so `NaN` equals `NaN` and `+0.0` does not equal `-0.0`.

code

java · 16 lines
java
@Test
void floatingPointTolerance() {
    // assertEquals(0.3, 0.1 + 0.2); // FAILS: actual is 0.30000000000000004
    assertEquals(0.3, 0.1 + 0.2, 1e-9);

    // absolute tolerance is hopeless at large magnitudes:
    double expected = 1_234_567_890.12;
    double actual = compute();
    assertEquals(expected, actual, Math.abs(expected) * 1e-9); // relative

    // bitwise overload: no delta argument
    assertEquals(Double.NaN, Double.NaN);      // passes (bit patterns match)
    // assertEquals(0.0, -0.0);                // FAILS: different bit patterns

    assertArrayEquals(new double[] {0.3, 0.6}, results(), 1e-9);
}

go deeper

for a junior

Show the 0.1 + 0.2 example and state the rule: floating-point results need a tolerance, and the delta is that tolerance.

for a middle

Explain the pass condition, the difference between absolute and relative tolerance, float versus double precision, and the bitwise semantics of the no-delta overload.

for a senior

Focus on choosing the tolerance from accumulated error and domain precision, on refusing to widen delta to hide defects, and on eliminating floating point for money and other exact domains.

for a principal

Treat it as a numerical-contract question: what precision the system promises its callers, where exact decimal types are mandatory, and what shared test helpers make relative comparisons consistent across the suite.

## Why exact comparison of doubles fails `double` and `float` follow IEEE 754: a number is stored as a sign, an exponent and a binary fraction. Only values expressible as a sum of powers of two fit exactly. `0.5`, `0.25` and `3.0` are exact; `0.1`, `0.2` and `1/3` are not — they are stored as the nearest representable neighbour. Every arithmetic operation then rounds its result to the nearest representable value, and those rounding errors accumulate. The canonical demonstration is `0.1 + 0.2`, which evaluates to `0.30000000000000004`. A test asserting the result is `0.3` with an exact comparison fails, and the code under test is not wrong — the assertion is. The same happens with `Math.sqrt`, trigonometry, iterative averages, currency conversion chains, and anything that sums many small values. ## What the delta overload does JUnit 5 provides `assertEquals(double expected, double actual, double delta)` (and the `float` equivalent). It passes when the two values differ by at most `delta`; internally it first checks the exact/bitwise equality path, then `Math.abs(expected - actual) <= delta`. Because the exact path runs first, `assertEquals(Double.NaN, Double.NaN, 0.0)` passes even though `NaN - NaN` is `NaN` and no comparison with `NaN` is ever true. A negative delta is rejected as an illegal argument. There is also a two-argument `assertEquals(double, double)` overload with no delta. It does **not** use `==`; it compares the raw bit patterns (equivalent to `Double.valueOf(a).equals(b)` / `doubleToLongBits`). Two consequences follow, and both surprise people: `NaN` equals `NaN` under this overload, and `+0.0` does **not** equal `-0.0`. Use it only for values you expect to be bit-for-bit identical — a constant you stored and read back, a value passed straight through, a parsed literal — never for a computed result. ## Picking a delta There is no universal value. Reason about three things: 1. **Magnitude.** Double precision gives roughly 15–16 significant decimal digits. The spacing between representable doubles near 1.0 is about `2.2e-16`; near 1e9 it is about `1e-7`. An absolute delta of `1e-9` is generous for values around 1 and *impossible to satisfy* for values around 1e9. That is why an absolute tolerance is only safe when you know the scale. 2. **Accumulated operations.** A single multiplication introduces about one unit in the last place of error; summing a million values can drift far more. Scale the tolerance with the length of the computation. 3. **Domain meaning.** If the value is a temperature displayed to one decimal, a delta of `0.05` is honest and readable. Choose a tolerance that expresses the precision the feature actually promises. For scale-independent checks, use a **relative** tolerance: `assertEquals(expected, actual, Math.abs(expected) * 1e-9)`. This says "within nine significant digits" regardless of whether the value is 3 or 3 billion. Guard the case where `expected` is 0 — a relative tolerance collapses to 0 there, so fall back to a small absolute floor. ## Anti-patterns - **Delta as bug repellent.** A test that only passes with `delta = 0.5` on a value of 2.0 is not testing much; either the algorithm is wrong or the assertion is asserting the wrong thing. Tolerances should be near the noise floor, not near the signal. - **`delta = 0.0`.** Legal and equivalent to the strict path, but if you want exactness, use the no-delta overload and say so. - **Floating point for money.** Currency should not be a `double` at all. Use `BigDecimal` (and be aware `assertEquals` on `BigDecimal` uses `equals`, which compares *scale* too — `new BigDecimal("1.0")` is not `equals` to `new BigDecimal("1.00")`; compare with `compareTo` or normalise the scale first) or store minor units in a `long` and assert exactly. - **`float` deltas copied from `double` tests.** `float` has ~7 significant digits, so `1e-9` is below its resolution and behaves like an exact comparison. ## Arrays of doubles The same tolerance problem applies element-wise to arrays, and JUnit covers it: `assertArrayEquals(double[] expected, double[] actual, double delta)` and the `float[]` equivalent apply the tolerance to each element pair. Without the delta argument, array elements are compared with the same bitwise semantics as the no-delta scalar overload. ## How to answer well The strong answer names the cause (binary representation plus rounding, not JUnit being fussy), states the pass condition (`|expected − actual| <= delta`), distinguishes absolute from relative tolerance, and finishes with the engineering judgement: pick the tolerance from the domain and the arithmetic, and keep money out of floating point entirely.

  • What does JUnit 5's two-argument assertEquals(double, double) — the one without a delta — actually compare?
    It compares the raw bit patterns of the two doubles, the same semantics as `Double.doubleToLongBits` or `Double.valueOf(a).equals(b)`, not `==`. So `NaN` equals `NaN` (unlike the `==` operator) and `+0.0` does not equal `-0.0` (unlike `==`). It is appropriate only for values expected to be bit-identical, such as a constant round-tripped through storage, never for a computed result.
  • A colleague set delta to 0.5 to make a flaky assertion pass. What is wrong with that?
    A tolerance that large is no longer absorbing floating-point noise — it is hiding a genuine numerical difference, so the test would still pass if the algorithm were meaningfully wrong. The right move is to find why the values differ: an accumulation-order problem, a unit or rounding-mode mismatch, or a real bug. Tolerances belong near the precision floor of the computation, not near the size of the quantity being asserted.
  • How would you assert on monetary amounts instead?
    Keep money out of `double` entirely: use `BigDecimal` or a `long` count of minor units, and assert exactly. If you use `BigDecimal`, remember that `assertEquals` calls `equals`, which also compares scale, so `1.0` and `1.00` are unequal — either normalise with `setScale` before asserting, or compare with `compareTo` inside an `assertTrue`/`assertEquals(0, a.compareTo(b))`.

A delta is the tolerance stamped on a machined part: 10 mm ±0.01 mm. Demanding an exactly-10.000000 mm part means every part fails inspection, even the good ones.

saying these in an interview costs you the question

  • Claiming JUnit 'has a bug' or that doubles are broken, rather than naming binary representation and rounding as the cause.
  • Using one habitual delta like 0.0001 everywhere regardless of magnitude, including on values in the billions where it can never be met.
  • Inflating the delta until a failing test goes green, masking a real numerical defect.
  • Believing the no-delta double overload uses ==, and being surprised that NaN equals NaN and +0.0 does not equal -0.0.
  • Using double plus a delta for currency instead of BigDecimal or minor units.

context