Estimated reading time: 7min
Over the years I have witnessed quite a few conversations where people argued about Test-Driven Development. And of course I don’t remember them all in detail, but one pattern I certainly noticed over time is that they often argue about the wrong thing. They argue about testing overhead, about coverage percentages, about whether mock-heavy unit tests are useful, or about whether TDD is “still worth it” in the age of LLM-assisted coding. But TDD never was about giving us more tests. It always has been about changing how we think while we write code.
That distinction matters.
Let me share a short trip into my past: Some (maybe some more) years ago, when I was still a junior developer, I thought I knew what TDD was about. After all, I had read quite a few books and articles about it. I had been practicing it for a while. I had even seen some of the benefits. But I was stuck in a mindset where I was primarily “testing with the intent of finding errors”. I thought of TDD as a way to get more tests, to catch more bugs. I did see the benefit of having a safety net for refactoring though – but who has time for refactoring, right?
Fast-forward a couple of years. I had the opportunity to join a TDD training with Craig Larman. I was a bit resistant at first, I didn’t want to occupy a spot in the training that could have been used by another team member. But someone got sick in the last minute and I decided to join, knowing of course that I already knew everything about TDD. Boy, was I wrong.
The training was technically not only about TDD, but also about mob programming. And about design. And about problem solving. You could even say it was not about testing at all. It was about thinking in smaller steps. It was about making my thinking visible. It was about learning how to ask better questions about the code I wanted to write.
And this is why the distinction matters: If you think of TDD as “a way to get automated tests”, it can easily look expensive. You write a test, then the production code, then refactor the design, and all of that seems slower than just jumping directly into the implementation. But if you think of TDD as a way to make your thinking visible, to force smaller decisions, and to tighten the feedback loop between idea and implementation, then something else comes into focus: TDD is less about checking code after the fact and more about shaping the code while it is still cheap to change. We do call it SOFTware for a reason.
And this is why some of the strongest TDD practitioners keep repeating a point that is easy to miss: the value of TDD is not mainly in the regression suite. The value is in the discipline of asking, before we write code, what we actually want the code to do.
TDD as in Thinking-Driven Development
Without TDD, coding sessions often start something like this:
“I probably need a class here.”
“This should become a service.”
“Let me add a few conditionals and a helper.”
While none of that is technically wrong, it means we start by thinking about structure rather than behavior. We start with our preferred solution shape instead of the observable effect we want. And that is often where code starts to drift: We design from the inside out.
And TDD helps us to flip that. It asks a different first question:
What should be true for a user, caller, or collaborating component when this piece of code works?
That question is much more useful than “What class should I write?” because it anchors us in behavior. The test becomes a tiny statement of intent. Not a bureaucratic artifact. Not a compliance checkbox. A statement of intent. Once you start there, code changes. Interfaces become simpler. Unnecessary dependencies become more obvious. Large leaps in implementation become harder to justify. And that is exactly the point.
The TDD loop is usually described as:
- Red: write a failing test
- Green: make it pass
- Refactor: improve the design without changing behavior
Which sounds easy and mechanical, but in practice it is a loop for disciplined thinking. The red-green-refactor is not a testing ritual. It is a way to keep switching between three different mental modes on purpose:
- specifying behavior
- implementing the smallest next step
- improving design
Many coding problems get harder because we try to do all three at once. And one of the biggest traps when developing software is premature abstraction. We invent frameworks for problems we do not yet understand. We introduce extension points before we have concrete variation. We generalize because we are afraid of changing code later.
TDD pushes against that habit.
And that is also why TDD often feels slower, especially in the beginning. That feeling is real, but it is not the whole story.
It feels slower because thinking is slower than typing.
If your usual mode of programming is to move quickly from idea to implementation, TDD interrupts that flow. It inserts a moment of reflection before each small step. That interruption can feel like friction. But a lot of what feels like speed in software development is actually deferral. We skip the hard thinking now and pay for it later in debugging, rework, unclear interfaces, and accidental complexity.
TDD In The Age Of LLMs
This is turning into a pretty long post already, so I will omit a few details on TDD here and would refer to the curiosity reading material or any other of the TDD-related content out there. But I want to briefly touch on the question of whether TDD is still relevant in the age of LLM-assisted coding.
I actually think TDD becomes more relevant, not less, when LLMs generate more code for us. And so does Dave Farley in a recent video on the topic that you can find in the curiosity reading material below.
Why? Because the bottleneck was never code production. The bottleneck is still understanding the problem, constraining the solution, and noticing when the code is wrong in small but important ways. If LLMs make it easier to generate a lot of plausible code very quickly, then the ability to guide development with small, behavior-oriented checks becomes even more important. Otherwise we simply accelerate the production of code we have not thought through properly. LLM-assisted coding will accelerate whatever we are doing, and if we are not careful, it will accelerate our mistakes as well. Or, as a former colleague of mine once said: “Shit in – shit out, but on steroids.”
I want to put it differently: If you do not know what question the next test should ask, then having a machine produce more implementation faster will not save you. TDD remains useful because it anchors development in intent. And intent remains to be the hard part.
Maybe start by asking:
What is the next smallest thing I want to learn about this code? That is usually where the interesting design work begins.
Until then: Happy hacking!
Curiosity Reading Material
- [BLOG] Transformation Priority Premise
- [BLOG] Canon TDD
- [ESSAY] Programming as Theory Building
- [VIDEO] Why AI WON’T Replace Software Engineering…
A Final Warning
There are a few – shall we say common – ways to miss the benefits of TDD – even though you are still performing the TDD loop. I will just leave them here so you don’t have to discover them the hard way:
- Writing tests after the design is already fixed in your head
- Specifying too much at once
- Testing internals because the behavior is still unclear
- Using TDD only at the unit level while ignoring larger feedback loops
- Treating refactoring as optional
Number 5 is my favorite one, the one that I have probably seen the most. Please remember: Refactoring is not a cleanup phase for later. It is part of the method. Without it, even TDD degenerates into incremental clutter.
