CodeOath
← All posts
Testing36 min total · 12 parts

Testing Fundamentals: Unit, Integration, and E2E Tests Done Right

Part 2 of 12 · ~4 min

Why We Test

A test is a program that checks whether another program does what it's supposed to do — automatically, repeatably, without a human clicking through the app by hand every time something changes. That last part is the entire economic case for testing: manual verification doesn't scale past a handful of features, because every new feature multiplies the number of things a human has to re-check by hand before every release.

The testing pyramid

Not all tests are the same shape, speed, or cost, and a healthy test suite is deliberately lopsided toward the cheap end:

        ▲
       / \        End-to-End (few)
      /---\       — slow, realistic, brittle
     /     \
    / Integ.\     Integration (some)
   /---------\    — moderate speed, real collaborators
  /           \
 /    Unit      \  Unit (many)
/-----------------\ — fast, isolated, cheap
  • Unit tests exercise one function, one class, one module in isolation — no real database, no real network, no real filesystem. They run in milliseconds, there are hundreds or thousands of them, and each one pinpoints a failure to almost the exact line that broke.
  • Integration tests exercise several real pieces wired together — your code talking to an actual (often local or in-memory) database, or two of your own modules collaborating for real, rather than through a stand-in. Fewer of these, and each one is slower and covers more ground per test.
  • End-to-end (E2E) tests drive the whole system the way a real user would — a real browser clicking through a real UI against a running backend. There are the fewest of these, they're the slowest by a wide margin, and they're the most realistic.

The ratio matters because it's a direct trade of confidence per test against cost per test, multiplied by how many of each you can actually afford to run. A thousand fast unit tests running on every keystroke in your editor, a hundred integration tests running on every push, and a dozen E2E tests running before a deploy is a suite you'll actually run constantly. Flip that pyramid upside down — mostly E2E tests, hardly any unit tests — and you get a suite that takes 40 minutes to run, fails for reasons unrelated to the actual bug about a third of the time, and gets skipped under deadline pressure specifically because it's so expensive to run and so hard to debug when it goes red.

Common mistake: treating "more E2E tests" as strictly more thorough, and therefore strictly better. E2E tests catch real integration and rendering problems unit tests structurally cannot see — but they're also the slowest, flakiest, and hardest to debug category by a wide margin, since a failure could be your code, a timing issue, a stale test database, or the test framework itself. The pyramid shape exists because confidence-per-second-of-CI-time drops sharply as you move up it, not because higher layers are somehow "worse tests."

The cost curve of a bug, by where it's caught

The same defect costs wildly different amounts to fix depending on how far it traveled before someone noticed:

Where the bug is caughtTypical cost to fixWhy
Editor / while writing the codeSecondsYou still hold the whole context in your head; nothing has been committed
A failing unit test, pre-commitMinutesStill local, still fresh context, no one else has touched the code yet
Code reviewTens of minutesSomeone else has to re-derive context; a round-trip of comments and re-pushes
CI on a pull request (integration/E2E)An hour or moreFull pipeline re-run, possibly blocking other people's merges
ProductionHours to days, plus damageIncident response, rollback or hotfix, customer impact, a post-mortem, and the original author has usually moved on to something else mentally

This is the actual argument for testing early and often: it's not that bugs in production are merely "annoying," it's that the same bug gets measurably more expensive the later it's discovered, because more work has been built on top of it and more people have to re-acquire context to fix it. A unit test that takes ten seconds to write and catches a bug the moment it's introduced is one of the highest-leverage ten seconds available to a working engineer.