The blind spot in a mocked test
When a unit test hands notify_user a Mock instead of the real email sender, the one thing it cannot tell you is whether the real email system actually delivers the message.
Doubles isolate the unit, which is their power and their blind spot at the same time. Everything about the interaction is verified, meaning the arguments, the formatting, and the call count, and the real dependency never ran even once.
Nothing checks that the sender's function is still named send, that it still takes two arguments in that order, or that the mail server is reachable. Those are facts about a real component and a real contract, and verifying them takes a different kind of test, which is this unit's topic.
Three altitudes of testing
Everything so far was unit testing: one function, dependencies replaced by doubles. Two more levels exist above it.
- an integration test wires several real pieces together and checks the seam between them: the parser feeding the calculator, your code talking to a real (test) database
- an end-to-end (E2E) test drives the whole running system the way a user would: open the browser, click checkout, see the receipt
Going up the levels buys realism and costs speed and stability. A unit test runs in microseconds and fails for one reason. An E2E test takes seconds or minutes, needs the whole app running, and can fail for a dozen unrelated reasons (a slow server, a renamed button).
What to test where
The pyramid is a budget, and each level has a specialty:
- unit: all your business logic and edge cases. The lesson 3-1 checklist lives here, where probes cost microseconds
- integration: the seams. Anywhere two components exchange data, or your code meets a real database, file system, or API contract
- end-to-end: two or three critical user journeys, such as sign up, pay, and see the dashboard
The top level is where smoke tests live, which are quick checks whose only question is whether the assembled system basically works at all. The name comes from hardware engineers powering on a new circuit board and watching for literal smoke. Smoke coverage is the goal there, not thoroughness.
The three levels differ in every dimension that matters for a suite you run all day:
| Level | Speed | Fails for | Count |
|---|---|---|---|
| unit | microseconds | one reason | hundreds |
| integration | milliseconds to seconds | a contract mismatch | dozens |
| end-to-end | seconds to minutes | many unrelated reasons | a handful |
The classic failure mode inverts the pyramid, with hundreds of slow end-to-end tests, few unit tests, and a suite nobody runs because it takes an hour and fails randomly. That is the trust problem from lesson 5-1 at the level of a whole suite. Keep the base wide and the top narrow.
Placing a discount rule
A rule reading "discount is 15% for carts over 100, boundary at exactly 100" belongs in unit tests, because it is pure business logic with cheap boundary probes.
Pure logic with interesting edge cases is exactly what the wide unit base is for. The lesson 3-2 boundary trio at 99, 100, and 101 costs microseconds there, so writing all three is free.
Running that same trio end-to-end would mean three full browser journeys to check three numbers, taking minutes instead of microseconds and failing whenever the checkout button gets renamed. The cost per probe is what decides the level.
An end-to-end test still has a job here, which is clicking through one discounted checkout to prove the rule is wired into the real flow. The distinction is worth keeping sharp: the top of the pyramid proves the feature is connected, and the base proves it is correct.