TDD as a Design Tool, Tradeoffs & Limits
Design Tool, Not Just Testing Tool
Writing the test first forces thinking about HOW code will be called (interface, inputs, outputs) before implementation details absorb attention. An awkward-to-test interface tends to reveal itself as awkward specifically because writing a clean test for it is hard — tests as a byproduct, better design as the real driver.
Test Behavior, Not Implementation
A test coupled to internal implementation details (not just observable behavior) breaks on refactoring even when external behavior hasn't changed — turning the test into an obstacle instead of an enabler. Writing tests against behavior, not internals, is what actually makes Refactor safe.
When TDD Is Harder to Apply
Exploratory coding, visually-judged UI work, or experimenting with an unfamiliar API can make writing a meaningful test upfront difficult. A "spike" — quick, disposable exploratory code with no TDD discipline — followed by a proper TDD reimplementation once the approach is understood, is a common pragmatic compromise.
Legacy Code Bootstrapping
Untested legacy code is often ALSO hard to add tests to (tight coupling, global state) — adopting TDD going forward frequently requires first investing in making existing code more testable before new TDD-driven work proceeds smoothly.
BDD & Proportional Rigor
Behavior-Driven Development extends TDD's "specify before implementing" idea into business-readable Given/When/Then specs (Cucumber-style), making the spec accessible to non-technical stakeholders. Applying TDD's discipline proportionally — heavier for complex business logic, lighter for trivial pass-through code — is a reasonable middle ground between strict universal adoption and skipping it entirely.
Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free