Skip to content

Overview — The Levels of Testing

You can prove a single function correct in a millisecond, and you can prove that a whole product does what a customer asked for — but those are not the same test, they do not cost the same, and they do not buy you the same confidence. The throughline of this book is how you earn justified confidence that software works, and how you think about the edge cases that break it. This part is where that question gets its first sharp answer: you earn confidence at different levels of scope, and the levels are not interchangeable.

A single test always fixes two things before it runs: how much of the system it puts under test, and what it lets stand in for the rest. Pin one function with everything else faked out, and you get a fast, precise check that says almost nothing about whether the pieces fit together. Boot the whole product and click through it like a user, and you get a slow, flaky check that — when it passes — tells you the thing actually works. Between those two extremes sits a spectrum, and the industry has named five useful points on it. This overview maps all five so the rest of the part has somewhere to slot in.

Imagine you refused to. You could try to earn all your confidence from one kind of test — say, boot the entire product and click through every feature. It would be realistic: a pass means the real thing really works. But it would also be agonizingly slow, maddeningly flaky (real networks, real clocks, real race conditions), and — when it went red — nearly silent about where the fault is. A failure could be a typo three layers down or a misconfigured load balancer; the test cannot tell you which. Debugging becomes a manhunt.

Now try the opposite: earn all your confidence from tiny isolated checks. They are fast and a failure points at one line — but they never wire anything real together, so a suite that is 100% green can ship a product where no two parts agree. You have proved every brick and never built the wall.

The escape from that dilemma is to stop treating “a test” as one thing. Split testing by scope, and each level gets to be excellent at one job: the small ones give you speed and a precise finger pointing at the fault; the large ones give you realism and meaning. You are not choosing between fast and realistic — you are buying both, at different levels, and composing the results. That composition is the entire subject of this part.

A testing level is defined by the scope of the system under test (SUT) — how much of the real system a test exercises, and where it draws the boundary between “real” and “substituted.”

scope of the system under test ─────────────────────────────────►
one function a few modules one service many services the product
in isolation wired together end to end + real infra a user sees
│ │ │ │ │
UNIT INTEGRATION SYSTEM END-TO-END ACCEPTANCE
│ │ │ │ │
◄── faster, cheaper, more of them slower, costlier, fewer of them ──►
◄── "is this piece right?" "did we build the right thing?" ──►

Move left and a test is fast, isolated, and cheap — so you write thousands of them, and a failure points at one function. Move right and a test wires more real parts together — so it is slower, more expensive, more prone to flake, and you write far fewer, but a pass means more, because more of the real system was involved. Every level is a deliberate trade of speed and precision against realism and confidence. No level is wrong; each answers a different question.

The five levels and the one question each answers

Section titled “The five levels and the one question each answers”
LevelScope under testThe one question it answers
UnitOne function/class, dependencies fakedIs this piece correct on its own?
IntegrationA few real units wired togetherDo these pieces agree at their seams?
SystemOne whole service/app, deployedDoes the assembled thing work?
End-to-end (E2E)The product + real dependencies, driven like a userDoes a real user journey work start to finish?
AcceptanceThe product against the requirementDid we build the right thing, per the spec?

The jump from system/E2E to acceptance is a change in kind, not just scope. The first four ask “does it work?” (correctness). Acceptance asks “is it the thing the customer needed?” (fitness for purpose). A system can pass every correctness level and still fail acceptance because it flawlessly implements the wrong thing — the most expensive bug there is.

Level maps to speed, cost, and kind of confidence

Section titled “Level maps to speed, cost, and kind of confidence”

The critical idea — the one that makes the whole part cohere — is that a passing test at each level buys you a different currency of confidence. Two tests can both be green and mean utterly different things. Green does not have a single meaning; its meaning is set by the level. Here is what each level’s green actually promises:

  • A green unit test says: this logic is right. It says nothing about whether the unit is even called, or wired to anything real.
  • A green integration test says: these two real parts agree — the serializer and the parser use the same format; the service and the database speak the same SQL.
  • A green system/E2E test says: the assembled product does the job — through real I/O, real config, real timing, the parts that only exist once everything is deployed.
  • A green acceptance test says: this is what the customer asked for — the feature is not just correct, it is the right feature.

You cannot substitute one for another. Ten thousand unit tests will not catch a mismatched API contract (that is integration’s job). A perfect E2E suite will not tell you the product solves the user’s actual problem (that is acceptance’s job). Confidence is layered, and each layer catches a class of bug the others structurally cannot.

Under the hood — what actually pins a test to a level

Section titled “Under the hood — what actually pins a test to a level”

Nothing in a test framework labels a test “unit” or “integration.” The level is an emergent property of two choices you make in the test’s setup:

  1. What you construct for real. If the test builds the real database, the real HTTP client, the real filesystem — its scope is wide. If it builds one object and hands it fakes for everything else, its scope is narrow.
  2. Where you cut with a test double. A test double (mock, stub, fake, spy) is a stand-in that replaces a real collaborator. Every double you insert is a boundary you have decided not to test right now — you are asserting “I trust this seam, or I test it elsewhere.” Move that boundary and the test’s level moves with it.
unit test: integration test:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SUT │──► [fake DB] │ SUT │──►│ real DB │
└──────────┘ └──────────┘ └──────────┘
one object, everything two real parts, the seam
past the boundary faked between them under test

This is why the same code can be tested at several levels, and why “how many doubles did you use?” is a sharper question than “which level is this?” A test with zero real collaborators is a unit test; a test with zero doubles and a running product is E2E; the interesting engineering lives in the middle, deciding which seams to make real.

Read these in order. Each page takes one level (or one decision about the levels), teaches it from first principles, and names exactly what a passing test at that level lets you catch.

#PageWhat it teachesWhat it lets you catch
2Unit Tests — Fast, Isolated, ManyTesting one unit in isolation; test doubles, arrange-act-assert, why they are the foundationLogic errors in a single function — off-by-ones, bad branches, wrong math
3Integration Tests — Where Units MeetTesting real components together across a seam; contracts, real dependencies, the diamond of doomMismatched contracts and broken wiring between parts that each pass in isolation
4System & End-to-End Tests — The Whole ThingDriving the deployed product like a user; the cost of realism, flakiness, and how to keep few of themConfig, timing, and integration bugs that only appear once everything runs together
5Acceptance Tests — Did We Build the Right Thing?Testing against the requirement, not the code; Given-When-Then, and the correctness-vs-fitness gapBuilding the wrong thing correctly — features that pass every test but miss the need
6The Pyramid vs the Trophy — Choosing a ShapeThe two dominant shapes for how many tests to keep at each level, and what each optimizes forOver-investing at the wrong level — a slow, flaky suite or a fast one that proves nothing
7Composing the Levels into a StrategyCombining all five into one deliberate confidence budget for a real codebaseGaps and overlaps — the same bug tested five times while another class is untested
Revision — The Levels of TestingA prose recap of the whole part

Here is the one idea to carry through every page: no single level is enough, and the skill is composing them. This part is not a contest to crown the “best” level — that is a category error, like asking whether a microscope is better than a telescope. Unit tests give you speed and precision; acceptance tests give you meaning; the levels in between stitch those extremes into a whole. The pyramid-vs-trophy debate (page 6) is not about which level wins — it is about the ratio you keep, and the composing page (page 7) turns that ratio into a strategy for a specific codebase. Every page that follows earns you one layer of the confidence stack. The goal is the finished stack, not any one brick.

To make this concrete, the book leans on two small real Rust projects — a key-value store (kvlite) and a log tool (logwise). Their tests/ directories already hold exactly the integration-level tests you will meet on page 3 (for example, kvlite/tests/concurrency.rs hammers one shared store from eight threads, and kvlite/tests/server.rs drives the real TCP server through a real socket). Watch how the scope of those tests grows as we climb.

With the map in hand, start at the bottom of the scope spectrum, where tests are fastest, most numerous, and most precise: Unit Tests — Fast, Isolated, Many. Every later page adds a layer on top of the one before it, until the whole stack stands.

  1. Two things are fixed before any test runs, and together they define its “level.” What are they?
  2. Name the five levels in order of increasing scope, and give the one question each answers.
  3. Why can a system pass every unit, integration, and E2E test and still fail acceptance? What class of bug is that?
  4. The “By the numbers” note shows unit tests are ~200x cheaper per test than E2E tests here. Why does that ratio not mean “just write only unit tests”?
  5. Restate the thread of this part in one sentence, without naming any single level as the winner.
Show answers
  1. The scope of the system under test (how much of the real system runs) and the boundary between real and substituted (what is faked out to stand in for the rest). Together they place the test on the level spectrum.
  2. Unitis this piece correct on its own? Integrationdo these pieces agree at their seams? Systemdoes the assembled thing work? End-to-enddoes a real user journey work start to finish? Acceptancedid we build the right thing, per the spec?
  3. Because those four levels test correctness (“does it work?”), while acceptance tests fitness for purpose (“is this what was asked for?”). A product can flawlessly implement the wrong requirement — it is correct and useless. That is the “built the right thing wrong” class: the most expensive bug, because everything downstream passed.
  4. Because unit tests buy a different currency of confidence. They prove logic is right but say nothing about whether real parts agree at their seams, or whether the deployed product does the job, or whether it is the right product. Ten thousand cheap unit tests structurally cannot catch a mismatched contract or a wrong requirement — those are other levels’ jobs. Cost per test is only one axis; the kind of bug caught is the other.
  5. Confidence is layered — you compose all five levels, each catching a class of bug the others cannot, rather than picking a single “best” level. (Any phrasing that keeps “compose, don’t crown a winner” is correct.)