Software Craftsmanship & Practice
Testing Philosophy
Testing is as much a design activity as a verification one — code that resists being tested is usually poorly decoupled — and the test pyramid balances speed against confidence rather than prescribing one "correct" ratio.
- Test pyramid: many fast, isolated unit tests; fewer, slower integration tests; a handful of end-to-end tests — a cost/confidence trade-off, not a rulebook
- Test-induced design pressure: code that is hard to unit test is usually too coupled to its collaborators — see Dependency Inversion And Injection as the fix
- Example-based tests assert specific input→output pairs; property-based tests assert an invariant holds across generated inputs, often finding edge cases nobody thought to hand-pick
- "Test early, test often, test automatically" — a suite that isn't run continuously isn't a safety net, it's a false sense of one
- An inverted pyramid ("ice-cream cone" — mostly slow, brittle end-to-end tests, few unit tests) is slow to run and flaky to maintain
| Layer | Speed | Isolation | Typical count |
|---|---|---|---|
| Unit | Milliseconds | One class/function, collaborators faked | Hundreds to thousands |
| Integration | Seconds | Several real components together (e.g. DB access) | Dozens to hundreds |
| End-to-end | Seconds to minutes | The whole system, as a user would exercise it | A handful of critical paths |
A class that is hard to unit test — it needs a real database connection, a live network call, three collaborators constructed just so — is very often exhibiting a coupling problem, not a "this kind of code just can't be tested" problem. Following the resistance back to its source and fixing the coupling (usually via Dependency Inversion And Injection, substituting a test double for the awkward collaborator) tends to improve the production design too: the two goals point the same direction more often than not.