The Illusion of Test-driven development [TDD]
Most TDD explanations focus on procedures—red-green-refactor cycles—but miss the fundamentals entirely. TDD isn't a mechanical checklist; it's a problem-solving technique. This article challenges conventional TDD wisdom and explores what it means to "thinking backwards" and how unit testing adds value but isn't the primary concern.
Last modified on
In extreme programming, software products are built using an agile methodology called test-driven development—or TDD, for short. As a developer who’s practiced TDD, I’ve found that practically all explanations of TDD miss the mark.
TDD Explanations Fail
Kent Beck, who coined TDD, never quite explained what it is in his book “TDD by Example”. Instead, he shows hands-on examples of how he applies the practice. He summarizes it as red-green-refactor using unit testing. Search online to learn what TDD is, and most explanations start with a red-green-refactor diagram. But this explanation is misleading because it emphasizes processes and tools over individuals and interactions—contradicting the first value in the agile manifesto. Telling someone that TDD means “step 1: write a failing test” is like telling them that agile means “step 1: attend morning stand-up meeting”. When we don’t understand the why, we blindly follow the process and do it wrong. Just like how people misuse stand-up meetings for communication rather than coordination.
Sometimes we say we test-drive our code to prove it works as the business expects. But here’s the thing: we can just as easily write code first, then write tests afterward to show we’ve met requirements. TDD skeptics are right to question this—and I say that as someone who’s practiced TDD for years and found it valuable. But I’m not here to sell you on TDD. Instead, I want to share a perspective you probably haven’t heard before—one I haven’t seen explained by any of the agile pioneers like Kent Beck, Martin Fowler, or Uncle Bob.
When Tests Don’t Drive Development
I want to walk you through a contrived example. I used to work in an environment that practiced TDD, and for a while, I helped teach 2-week boot camps to onboard employees and teach them XP, including TDD. The first coding assignment when learning TDD was the Fahrenheit-to-Celsius problem.
The units of measure of temperatures are given in Fahrenheit [F] and Celsius [C] degrees. The test cases that must pass are as follows:
- 32 F degrees must be 0 C degrees.
- 212 F degrees must be 100 C degrees.
- 50 F degrees must be 10 C degrees.
To pass the first test, the function can just return 0, but the second test forces us to solve the formula: (f - 32) * 5 / 9. It’s straightforward. Students always solve it, and they do so using TDD as they were taught—red-green-refactor. Now suppose there’s a similar exercise with an unknown unit of temperature measure [X]. Let’s try to solve the equation to convert Fahrenheit to this unknown measure. The test cases that must pass are as follows:
- 18 F degrees must be 0 X degrees.
- 212 F degrees must be 4 X degrees.
- 115 F degrees must be 2 X degrees.
Just like before, to pass the first test, we can just return 0. Easy enough. The second test forces us to solve the formula. But what is the formula? Unlike the Fahrenheit-to-Celsius problem, we’re now stuck—we don’t know how to convert Fahrenheit to this unknown measure. That’s because our tests never actually drove our code, despite following the procedural TDD steps of iteratively writing a failing test first.
TDD Is A Way Of Thinking
TDD is a problem-solving technique that cognitive psychologists call “thinking backwards” (or “working backwards”). When we think backwards, we drive with the business requirements and work our way to a solution. In contrast, thinking forward means we start with facts, data, or prior knowledge. In programming, the code represents the data, and tests represent the business requirements. When thinking forward, we write the code—say, (f - 32) * 5 / 9—and then check if it satisfies the requirements. When thinking backwards, we start with the business requirements (the tests) and search for the code that satisfies them.
How we search matters. Uncle Bob introduced the Transformation Priority Premise [TPP]—an ordered list of inference rules that helps us work backwards1. Still, most problems that average developers face daily don’t involve algebraic manipulation, so following Uncle Bob’s TPP won’t cover this domain. Instead, developers need to break down TPP’s second rule (constant -> constant+) into more complex rules: Addition (+,-) -> multiplication (×,/,%) -> exponentiation (^, log) -> trig (sin, cos) -> …
So, if we apply the simplest transformation (constant -> constant+addition), then, to keep the first test passing, we would transform 0 to f - 32. Let’s experiment with the next (constant -> constant+) transformation: multiplication. This transformation actually makes a lot of sense because any number multiplied by zero is zero, so our first test case will remain passing. We just need to find the right factor to multiply c = (f - 32) * x. Solving the factor algebraically is simple if we substitute our expected Fahrenheit to Celsius constants 212 and 100, respectively:
x(f - 32) = c
x = c / (f - 32)
= 100 / (212 - 32)
= 100 / 180
= 5 / 9
The Fahrenheit-to-Celsius problem is deceptively simple. Mathematically, it’s just a linear equation—a line on a Cartesian plane. And since a line is completely determined by exactly two points, writing tests first doesn’t actually add value here. One test case forces us to solve the entire equation, which perfectly illustrates that writing tests first and thinking forward is the illusion of test-driven development.
If a developer used algebraic manipulation to solve this problem without writing a unit test, I’d argue they test-drove more effectively than the developer who iteratively wrote unit tests while thinking forward. That’s because TDD shouldn’t be viewed as a test-oriented development method. The unit tests aren’t the main point. The tests don’t literally drive development—the developer is still the driver, deciding which test to solve next. In that sense, TDD is a misnomer.
The reason developers write unit tests iteratively in TDD is that thinking backwards is memory-intensive. We need to hold key assumptions in mind and verify they still hold as we continue reasoning backwards. It’s been shown that humans struggle to hold large amounts of information in working memory 2. However, when we test-drive, the key information we need while thinking backwards is preserved in automated tests. Intensive working memory is offloaded to the computer, which is far better suited for the task. Each step is validated instantly, making it easy to backtrack when we get stuck. Test-driving makes thinking backwards simpler and more practical.
What’s Next
I’ll end this article here, but I have more to say about this topic. For one, there are interesting observations about how novices and experts differ in their use of backward versus forward thinking when solving problems, plus important discussions about how experts can sometimes be overconfident. Also, in neuroscience, there’s no evidence of backward thinking—that’s just psychology jargon used to describe human behavior from an outside observer’s perspective. Instead, researchers believe humans only think forward, which makes sense: after all, problem solvers need prior knowledge of inference rules to apply when “thinking backward.”
Also, concluding that TDD can only be applied with unit tests is incorrect. I’d like to narrow down what exactly TDD is by looking at Edwin Brady’s Type-Driven Development book, where he uses a dependent type system to iteratively construct a solution. This isn’t just another [fill-in-the-blank]-driven development method—it helps developers solve problems that unit tests don’t address. I’m also interested in exploring how AI systems might apply TDD principles to their own problem-solving processes. Hint hint: it’s not through LLMs, but rather through symbolic AI or the field of program synthesis.
-
Robert C. Martin (2013). The Transformation Priority Premise. https://blog.cleancoder.com/uncle-bob/2013/05/27/TheTransformationPriorityPremise.html. ↩
-
Miller, G. A. (1956). The Magical Number Seven, Plus or Minus Two: Some Limits on Our Capacity for Processing Information, Psychological Review, 63(2), 81–97. https://labs.la.utexas.edu/gilden/files/2016/04/MagicNumberSeven-Miller1956.pdf. ↩