Unit Testing Principles: FIRST, the Test Pyramid, and What Makes a Test Good
The pyramid-vs-trophy debate (Fowler/Google vs. Kent C. Dodds) with attribution, Google's small/medium/large size taxonomy and 80/15/5 split, the Luo et al. flaky-test root-cause taxonomy with real percentages, and mutation testing as the actual suite-quality signal coverage isn't.
Assume the reader already knows FIRST, Arrange-Act-Assert, and what a unit test is. This document covers what isn't textbook material: the named pyramid-vs-trophy debate, Google's actual internal ratios and vocabulary, the sourced flaky-test taxonomy, and why coverage percentage is not a suite-quality metric.
The Pyramid-vs-Trophy Debate: Two Named, Conflicting Positions
Position A - the classic pyramid (Mike Cohn, popularized further by Martin Fowler and Google's Testing Blog): many fast unit tests, fewer integration tests, very few end-to-end tests, ordered by cost/speed/flakiness. Google's own internal guidance ("Software Engineering at Google," ch. 11) states a concrete target ratio of 80% unit / 15% integration / 5% end-to-end, and its 2015 Testing Blog post "Just Say No to More End-to-End Tests" (Mike Wacker) argues E2E suites should be used sparingly because they are unstable and slow to diagnose.
Position B - the testing trophy (Kent C. Dodds, kentcdodds.com/blog/write-tests): static analysis (TypeScript/ESLint-class tooling) as a free base layer, then integration tests as the largest, most-invested layer, with unit and E2E both smaller. Dodds' stated reasoning is a confidence/cost trade-off, not a speed argument: "as you move up the pyramid, the confidence quotient of each form of testing increases," and integration tests get most of that confidence gain without full E2E cost. His companion aphorism, widely quoted verbatim: "Write tests. Not too many. Mostly integration."
These are not the same claim answered differently - they optimize for different failure modes. The pyramid optimizes for feedback speed and failure localization (Google's own codebase is enormous and refactored constantly by thousands of engineers; a unit-heavy suite gives fast, precise blame). The trophy optimizes for confidence-per-test in smaller, UI-heavy codebases where unit tests of individual functions frequently pass while the wired-together feature is broken (the "unit tests mocked the seam that was actually broken" failure - see Mocking Strategies).
Decision rule:
| Codebase shape | Lean toward |
|---|---|
| Large monorepo, many independent pure-logic modules, frequent large-scale refactors | Pyramid (Google ratio) |
| Small-to-mid app where most bugs live at component/module wiring boundaries (frontend, API-glue-heavy backends) | Trophy (integration-heavy) |
| Either - but end-to-end suite is the only safety net | Neither is satisfied; add unit and/or integration tests, don't add more E2E (Wacker's core argument) |
Google's Size Taxonomy Is a Better Vocabulary Than "Unit/Integration/E2E"
Google replaced "unit/integration/e2e" internally with small / medium / large / enormous, scoped by what the test is allowed to touch, not by what layer of the architecture it happens to exercise:
- Small: single process, no network, no sleeping, no blocking I/O - must run in milliseconds.
- Medium: can span processes on one machine, can hit
localhost, can touch disk. - Large/Enormous: multi-machine, real network, real external services.
This taxonomy is more decision-useful than the pyramid's layer names because it's enforceable by CI infrastructure directly (a test tagged small that opens a socket is a build error at Google, not a code-review nitpick) rather than relying on a human classifying which "layer" a test conceptually belongs to.
Flaky Tests: A Sourced Root-Cause Taxonomy, Not a Mystery
Luo, Hariri, Eloussi, and Marinov, "An Empirical Analysis of Flaky Tests" (FSE 2014; mir.cs.illinois.edu/lamyaa/publications/fse14.pdf) mined 201 flakiness-fixing commits across 51 open-source Java projects and produced the taxonomy most CI tooling still cites in 2026:
| Root cause | Share of studied flakes | Fix pattern |
|---|---|---|
Async wait (fixed sleep() instead of polling/condition) | ~45% | Poll a condition with timeout; never bare-sleep. Fowler's independent guidance (martinfowler.com/articles/nonDeterminism.html) reaches the identical conclusion. |
| Concurrency (race conditions, atomicity violations, deadlocks) | ~20% | Eliminate shared mutable state between tests/threads; make the ordering deterministic under test. |
| Test-order dependency | ~12% | Function-scoped fixtures, no test writes to state another test reads; shuffle test order in CI (pytest-randomly) to surface this class proactively. |
| Resource leak | remainder, recurring category | Configure test resource pools small enough that a leak fails fast and loud instead of silently accumulating. |
| Network / external dependency | recurring category | Push out of the unit layer entirely - this is what the integration layer and Testcontainers exist for. |
| Time dependency | recurring category | Inject a Clock port; never call the system clock directly from logic under test. |
Google's own flaky-test writeup (testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html) independently confirms the same top causes at their scale (UI-harness timing, poor test isolation via shared global/temp-file state, unreliable external connections) and adds an operational pattern worth stealing directly: a quarantine system that reruns newly-flaky tests automatically, strips them out of the blocking critical path, and enforces a hard time limit before a quarantined test must be fixed or deleted - flaky tests left both flaky and blocking are what trains engineers to ignore red CI.
Decision rule for triage: when a test fails intermittently, check causes in the above order - async wait is the single most likely explanation before you assume something exotic. Never mark a test @retry/quarantine without opening a tracked ticket; a permanently-retried test is a silently disabled test.
Mutation Testing Is the Actual Suite-Quality Signal - Coverage Is Not
Line/branch coverage measures whether code executed during the suite, not whether a bug in that code would have been caught. A test with assert result is not None on a line that computes a completely wrong value still counts as "covered." Mutation testing (mutmut, cosmic-ray for Python; Stryker for JS/TS) tools deliberately inject small bugs (flip a comparison operator, off-by-one a boundary) and measure the kill rate - the fraction of injected mutants that make some test fail. A suite with 100% line coverage and a low mutation-kill rate is testing that code ran, not that it's correct - treat a coverage number alone as a floor to find untested branches, never as a suite-quality target.
When Not to Invest Further in Unit Tests
| Signal | Implication |
|---|---|
| Bugs keep reaching production despite green, high-coverage unit suite | The bugs live at wiring/integration boundaries the unit layer cannot see by construction - invest in the trophy's integration layer instead, don't add more unit tests of the same units. |
| A 5-line change breaks dozens of unit tests | Brittle, implementation-coupled suite (see Mocking Strategies) - fix the tests' coupling, don't treat the breakage count as validation. |
| Suite takes minutes locally | Something in the "unit" layer is doing real I/O; reclassify it as medium/integration per Google's taxonomy above and move it to that CI stage. |
Related Documentation
- Integration Testing - where the trophy/pyramid's middle layer is defined in this corpus.
- Mocking Strategies - the over-mocking failure mode that motivates the trophy's integration-heavy position.
- Clean Code Principles
Sources
- Kent C. Dodds, "Write tests. Not too many. Mostly integration" -
kentcdodds.com/blog/write-tests- origin of the testing trophy and the confidence-quotient argument. - Kent C. Dodds, "The Testing Trophy and Testing Classifications" -
kentcdodds.com/blog/the-testing-trophy-and-testing-classifications - Mike Wacker, "Just Say No to More End-to-End Tests" -
testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html- Google's pyramid-side counter-position. - "Software Engineering at Google," ch. 11 (Testing Overview) -
abseil.io/resources/swe-book/html/ch11.html- the 80/15/5 ratio and small/medium/large/enormous size taxonomy. - Luo, Hariri, Eloussi, Marinov, "An Empirical Analysis of Flaky Tests" (FSE 2014) -
mir.cs.illinois.edu/lamyaa/publications/fse14.pdf- the sourced root-cause percentages. - Martin Fowler, "Eradicating Non-Determinism in Tests" -
martinfowler.com/articles/nonDeterminism.html- independent confirmation of the async-wait/isolation/clock taxonomy plus the quarantine pattern. - Google Testing Blog, "Flaky Tests at Google and How We Mitigate Them" -
testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html- Google-scale flaky-test mitigation, quarantine system. - Google Testing Blog, "Test Sizes" -
testing.googleblog.com/2010/12/test-sizes.html- origin of the small/medium/large vocabulary.