Testing & QA

Unit tests — what 'unit' actually means

"Unit" doesn't mean "one function" — it means one behavior, tested in isolation from the things around it, which is a much more useful (and more debated) definition than the naive one.

Beginner

3 min read

The naive definition breaks immediately

function formatPrice(cents) {
  return `$${(cents / 100).toFixed(2)}`;
}
 
function getDiscountedPrice(cents, discountPercent) {
  const discounted = cents * (1 - discountPercent / 100);
  return formatPrice(discounted); // calls another function
}

If "unit" meant "exactly one function, and nothing it calls," then a test for getDiscountedPrice would somehow need to avoid also exercising formatPrice — which is both impractical and pointless, since formatPrice is a small, pure, deterministic helper that adds no real risk or unpredictability to the test. The useful definition of "unit" isn't about counting functions; it's about isolation from the things that make a test slow, flaky, or hard to reason about — a real network call, a real database, real wall-clock time, real filesystem access. getDiscountedPrice calling formatPrice isn't a problem; getDiscountedPrice calling out to a live payment API would be.

A unit test's actual job: one behavior, deterministic, fast

test("applies a 20% discount and formats as currency", () => {
  expect(getDiscountedPrice(1000, 20)).toBe("$8.00");
});

This test runs in well under a millisecond, produces the exact same result every single time it runs (no dependency on the current date, network latency, or database state), and if it fails, the failure points at a small, specific piece of logic rather than "something, somewhere in a large system, is wrong." Those three properties — fast, deterministic, and narrow enough to localize a failure — are what actually define a good unit test, far more than any rule about how many functions it's allowed to touch.

Where the boundary actually gets drawn: things that make tests slow or flaky

// NOT a good candidate for a unit test's boundary — real network I/O
async function fetchUserFromApi(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}
 
// A good candidate — pure logic, no I/O, safe to call directly and repeatedly
function isEligibleForDiscount(user) {
  return user.accountAgeMonths >= 12 && user.totalOrders >= 5;
}

isEligibleForDiscount is exactly the kind of thing a unit test should test directly, with plain function calls and real assertions — it's pure, fast, and deterministic by nature. fetchUserFromApi is exactly the kind of thing a unit test should not call directly: hitting a real network every test run makes the suite slow, makes it fail when the network or the remote API happens to be down (a failure that has nothing to do with whether your code is correct), and makes results non-deterministic. The next lesson (test doubles) covers the actual mechanism for testing code that depends on something like fetchUserFromApi without actually calling it for real.

Naming a unit test so a failure is self-explanatory

// Vague — a failure tells you almost nothing about what actually broke
test("discount works", () => { /* ... */ });
 
// Specific — a failure tells you exactly which behavior broke
test("returns false when account age is under 12 months, even with enough orders", () => {
  expect(isEligibleForDiscount({ accountAgeMonths: 6, totalOrders: 20 })).toBe(false);
});

A test name is read far more often in the failing case than the passing one — when a CI run goes red, the test name (not its body) is usually the first thing a developer sees, often without immediately opening the file. A name that states the specific behavior and the specific condition being checked turns a failure into "oh, I broke X" immediately; a vague name turns it into "something in this area broke, better go read the code to find out what."

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What does "unit" actually mean in "unit test," more usefully than "exactly one function"?

2. What three properties actually define a good unit test?

3. Why does a vague test name like "discount works" cause real problems?