ConstructiCat Logo
CodeBust.
Browse section ▾

Magic Number Test.

A test hard-codes unexplained numeric literals in its inputs and assertions, hiding what the numbers mean and where they came from.

##Signs and Symptoms

You spot a Magic Number Test when a test's arguments and assertions are full of bare numbers whose meaning and origin are not obvious from the code. The reader has to reverse-engineer (or just trust) why a particular value is expected.

Tell-tale signs:

  • Numeric literals appear directly as assertion arguments: expect(result).toBe(54.13), assertEquals(86400, ttl).
  • The same literal is repeated across setup, action, and expected value, with no name tying them together.
  • Numbers encode domain concepts that aren't spelled out (3600 = an hour, 200 = HTTP OK, 0.0825 = a tax rate).
  • A code comment sits next to the number to explain it — a sign the number itself should have been named.
  • During review people ask "why 42?" or "where does 54.13 come from?" and nobody can answer without re-running the code.
// Smell: what is 8.25? why 54.13? what's 50 hidden inside the helper?
test('checkout works', () => {
  const total = checkout(cartFor(50), 8.25);
  expect(total).toBe(54.13);
});

This is a specific, test-side flavor of the general Magic Number smell, and a classic contributor to the Obscure Test smell (Meszaros): the reader can't understand the test from the test alone.

##Reasons for the Problem

Why it happens

  • The literal is the path of least resistance: you type the value you saw in a debugger or copy the actual output from a failing run into the assertion until it goes green ("Guess the value" / output-pasting).
  • The author already has the domain context in their head, so 3600 or 8.25 feels self-evident at the time of writing.
  • Fixture values get picked arbitrarily (new User(25, ...)) just to make the constructor happy, with no thought to meaning.

Why it hurts

  • Readability / intent. A number like 54.13 states a fact but not a reason. Reviewers and future maintainers can't tell whether it's a deliberate expectation, a boundary, or an accident. The test stops being executable documentation.
  • Maintainability. When the rule changes (the tax rate, the timeout, the page size), you must hunt down every copy of the literal and know which 7 meant "days" versus "max retries." Unnamed duplication makes safe edits expensive and error-prone.
  • Reliability / false confidence. If the expected value is wrong — or right only by coincidence — nothing in the test reveals it. Worse, authors often "fix" a magic-number test by recomputing the expected value with the production formula (expect(total).toBe(subtotal * (1 + rate)))), turning the assertion into a tautology that re-implements the code under test and can never fail for the right reason.
  • Diagnosis. When such a test breaks, the failure message is just "expected 54.13, got 54.12" with no clue about which input or rule produced the number, slowing debugging.

##Treatment

Apply Replace Magic Number with Symbolic Constant (Meszaros): give every meaningful value a name that explains its role, and make the relationship between inputs and the expected result explicit.

Concrete steps:

  1. Name the inputs. Extract literals used as test data into well-named local constants or fixture-builder calls (const SUBTOTAL = 50.00, const TAX_RATE_PCT = 8.25). Use an Object Mother / builder for object fixtures so only the values that matter to the test are visible.
  2. Name and explain the expected value. Keep the expected result as an independent literal, but name it and document how it was derived (const EXPECTED_TOTAL = 54.13; // 50.00 + 8.25% tax). Do not recompute it with the production formula — that just retests the code against itself.
  3. Tie inputs to the assertion so a reader can verify the arithmetic by eye, or assert against a derived-but-independent reference value.
  4. Leave genuinely self-evident values alone. 0, 1, -1, array indexes, and obvious counts (items).toHaveLength(2)) usually don't need names; reserve constants for values whose meaning isn't self-explanatory. Promote a shared constant to a single source of truth only when it's truly the same concept everywhere.
// Before
test('checkout works', () => {
  const total = checkout(cartFor(50), 8.25);
  expect(total).toBe(54.13);
});

// After
const SUBTOTAL = 50.00;
const TAX_RATE_PCT = 8.25;
const EXPECTED_TOTAL = 54.13; // SUBTOTAL plus 8.25% sales tax

test('applies sales tax to the subtotal', () => {
  const total = checkout(cartFor(SUBTOTAL), TAX_RATE_PCT);
  expect(total).toBe(EXPECTED_TOTAL);
});

The names now carry the intent; if the tax rule changes, the edit is local and obvious, and the expected value stays an honest, independent check rather than a tautology.

##Detected by