TDD Does Not Mean Writing Tests for Everything. Here Is What It Actually Means.
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Straw Man Version
The version of TDD that most developers reject is this: before you write any code, you must write a test. Every class, every method, every configuration file must be test-driven. It is a strict process with no exceptions, and deviation from it means you are not doing TDD.
This version is a straw man. It is not what the practitioners who developed TDD actually advocate, and it is not what experienced TDD practitioners actually do. But it is what gets described in dismissive discussions, and it is the thing developers try once and reject.
The actual practice is more purposeful: apply TDD where it helps, skip it where it does not.
Where TDD Provides High Leverage
TDD is most valuable for code where the design of the interface is non-obvious, where the logic has multiple branches and edge cases, or where the behavior needs to be precisely specified before implementation begins.
Complex business logic. Discount calculations, eligibility rules, pricing tiers, state machines, workflow transitions — anywhere that has conditional logic and domain-specific rules. Writing the tests first forces you to enumerate the cases explicitly, which surfaces edge cases before they become production bugs.
Public APIs of core domain objects. The interface that other parts of the system depend on is worth designing through tests. The test is a first caller — the most direct way to experience whether the API is natural to use.
Code you are going to maintain for a long time. TDD produces well-specified, well-isolated code with good coverage. The cost is worth paying for code that will be read, modified, and extended repeatedly.
Where TDD Adds Less Value
Exploratory/spike code. When you genuinely do not know what the right approach is yet, writing tests before code is premature specification. Write the spike without tests to understand the problem space, then throw it away and rebuild test-first. The spike is research; the rebuild is engineering.
Trivial data transfer objects and configuration. A class that is three getters and a constructor does not benefit from TDD. The design is obvious, there is no logic to specify, and there is no edge case to discover.
Glue code and framework wiring. Code that connects your application to a framework — Spring configuration, dependency injection setup, route registration — is largely shaped by the framework's conventions. TDD does not add design leverage here because the design space is constrained by the framework.
Infrastructure and I/O code. Database repositories, HTTP clients, file readers — code whose primary behavior is interacting with external systems — is better covered by integration tests. TDD can still be applied, but the feedback loop is slower (you need a database running), and the design leverage is lower (the interface is often prescribed by the external system).
The Practical Application
A realistic TDD session on a feature looks like this:
- Identify the core logic that drives the feature — the part with the business rules and conditional behavior.
- Start TDD on that logic. Write the first test that specifies the simplest behavior. Make it pass. Write the next test for the next case. Refactor as needed.
- For the infrastructure code that the logic depends on (database access, external APIs), define the interface through the tests (by mocking it) and implement it separately — potentially without strict TDD if the interface is prescribed by the external system.
- Skip tests entirely for configuration, boilerplate, and one-liners that have no interesting logic.
# Apply TDD here: complex rules with multiple cases
class SubscriptionEligibilityChecker:
def is_eligible_for_upgrade(self, user: User, plan: Plan) -> EligibilityResult:
# Business logic with 6 conditions — worth TDD'ing
...
# Skip TDD here: trivial data class, no logic
@dataclass
class EligibilityResult:
eligible: bool
reason: str | None = None
The discipline is not "test before all code." It is "test before code where the test adds design or specification value." Applying that judgment consistently — rather than either ignoring TDD entirely or applying it dogmatically — is what experienced practitioners actually do.
TDD is a tool. Like any tool, its value depends on whether you are using it in the right situation. The question is not "am I doing TDD?" but "is writing this test before this code going to improve the design or specification of this behavior?" When the answer is yes, write the test first. When the answer is no, do not.