Testing & QA

Why we test — the cost of a bug found late vs. found now

The core economic argument for testing at all: the cost of fixing a bug scales with how far it travels before it's caught, and a test suite's whole job is catching bugs at the cheapest possible point in that chain.

Beginner

3 min read

The same bug, caught at four different points

Written by a developer, wrong from the start
        |
   [1] Caught while typing (IDE/type errors) -----> fix: seconds
        |
   [2] Caught by a test, before commit -------------> fix: minutes
        |
   [3] Caught in code review or CI ------------------> fix: an hour, a context switch
        |
   [4] Caught in production, by a user --------------> fix: hours/days, plus the
                                                         incident itself already happened

The bug itself doesn't change across these four points — same wrong line of code, same root cause. What changes is everything around it: how much has been built on top of the wrong assumption since it was introduced, how many people now need to be involved to diagnose it, and whether real users were affected before anyone noticed. A test suite's entire economic argument is that it moves the catch point from [4] or [3] back to [2] — automatically, on every single change, without anyone having to remember to think about that specific bug ever again.

Tests are a way of not having to re-verify things by hand, forever

// Without a test: every time this function changes, someone manually checks
// "does discount still work for a 10-item cart? for a 0-item cart? for a
// cart over the free-shipping threshold?" — by hand, in a browser, hoping
// they remember every case that ever mattered.
function calculateTotal(cart) { /* ... */ }
 
// With a test: the same checks run automatically, every time, in milliseconds
test("applies a 10% discount for carts over $100", () => {
  const cart = [{ price: 60 }, { price: 50 }];
  expect(calculateTotal(cart)).toBe(99); // 110 - 10%
});

The first time you manually verify a behavior, you're doing necessary work. The second, third, and fiftieth time you manually re-verify the same behavior — because you touched nearby code and want to make sure you didn't break it — you're doing work a computer could do in milliseconds instead. A test, once written, is that verification captured permanently: it runs the same way, checks the same thing, and never forgets a case the way a rushed manual check under deadline pressure might.

Tests are also a design tool, not just a safety net

Writing a test for a function forces you to actually call it the way a real caller would — with real inputs, checking a real output — before you've moved on to the next thing. A function that's awkward to test (needs five unrelated objects set up first, reaches into three levels of shared state, can't be called without also triggering a network request) is very often a function with a real design problem, not just a "hard to test" problem — the awkwardness in the test is usually a symptom of the same coupling that will make the function hard to reuse, hard to reason about, and hard to change later. This is why "write the test first" (covered later in this domain) is a workflow some teams swear by: it front-loads that feedback to before the design is locked in, rather than after.

What testing doesn't buy you

A passing test suite means "the things we thought to check still behave the way we expected." It does not mean "this code has no bugs" — it can't catch a case nobody thought to write a test for, and a badly written test can pass while checking almost nothing meaningful (a theme this domain returns to directly in a later lesson on weak assertions and coverage games). Testing is a real, measurable reduction in a specific category of risk — regressions in behavior that was tested — not a guarantee of correctness in general.

Further reading

Check your understanding

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

1. What's the core economic argument for having a test suite at all?

2. What does a passing test suite actually guarantee?

3. Why is a function that's awkward to test often a sign of a real design problem?