TDD: A Developer’s Best Friend or Worst Enemy?


The Debate Between: Martin Fowler, Kent Beck and David Heinemeier Hansson (DHH)
This article was inspired by this video: https://www.youtube.com/watch?v=z9quxZsLcfo
I’ve wanted to write about this for a long time because as developers testing our code is a crucial part of our work. The way we approach testing determines whether our tests become valuable assets or a burden that slows us down when we need to refactor, make changes, or upgrade our code.
Throughout my career, I’ve seen many fragile test suites that require excessive effort to fix every time we modify our code. This article, particularly the section I’m most interested in, addresses that issue—especially regarding mocks. I reference Kent Beck, and I absolutely loved what he had to say on the subject.
So, I invite you to stick with me and read through to the end. I also propose some alternative strategies to tackle these challenges effectively.
The Three Main Issues with TDD
David Heinemeier Hansson raised three key concerns regarding TDD and unit testing:
Definition Confusion – There is a lack of consensus on what TDD and unit testing truly mean, leading to different interpretations and implementations.
Test-Induced Design Damage – The use of mocks to drive architecture often results in unnecessary complexity and poor design choices.
The Red/Green/Refactor Cycle – The rigid structure of TDD never worked well for him and felt unnatural in his workflow.
Kent Beck, one of the pioneers of TDD, provided historical context, explaining that his approach evolved from experiments in Smalltalk. While TDD worked well for him, he acknowledged that it isn’t the only way to ensure reliable software.
Confidence in Code: Different Paths to the Same Goal
Martin Fowler highlighted an essential question: Can you sleep at night knowing that your code works? All programmers should strive for confidence in their code, but how they achieve that confidence can vary. TDD is one method, but there are many other ways to reach the same goal.
David emphasized his preference for Ruby’s philosophy of programmer happiness. He believes in self-testing code but finds TDD too rigid. Instead, he argues that developers should focus on strategies that suit their thought processes, rather than adhering to TDD dogma.
TDD as a Trade-Off: Weighing Costs and Benefits
Kent Beck pointed out that TDD is about making trade-offs. Designing code so that intermediate results are testable can lead to better structure, but it also introduces costs. For example, in compiler development, having an intermediate parse tree makes a great test point and results in a better design. However, enforcing testability for every intermediate step may not always be worth it.
A key takeaway from Beck's argument is the importance of balancing design decisions. He expressed concern that overuse of mocks leads to brittle tests and reduced flexibility.
The Mocking Problem and Test Fragility
One of the most insightful moments in the discussion was when Kent Beck addressed the issue of excessive mocking:
Do you mock absolutely everything? My personal practice is, I mock almost nothing*. If I can't figure out how to test efficiently with the real stuff, I find another way of creating a repeatable feedback loop. I just don't go very far down the mock path. I looked at code where you have mocks returning mocks returning mocks, and my experience is: if I can use TDD, I can refactor stuff. Then I heard stories from people saying, "Well, I use TDD, and now I can't refactor anything." I couldn't understand that. Then I started looking at their tests, and... well, If you have mocks returning mocks returning mocks, your test is completely coupled to the exact implementation—not the interface, but the exact implementation—of some object three streets away. Of course, you can't change anything without breaking the tests! So that's too high a price to pay. That's not a trade-off I'm willing to make.*
This point is crucial. Overuse of mocks can lead to tests that are fragile and too tightly coupled to the implementation. Instead of providing confidence, these tests make it harder to refactor and evolve the codebase.
Alternatives to Mocks: Using the Real System with Caching and Other Strategies
A more practical approach is using real dependencies while caching results to speed up tests. For example, if you need to test a system that interacts with an external API, such as Apache NiFi, instead of mocking its API responses, you can run tests against a real NiFi instance and cache the results for a fixed period. This approach ensures tests reflect real-world behavior while improving efficiency.
Other alternatives to mocks include:
Snapshot Testing – Capture the output of real interactions and compare against stored snapshots.
Service Virtualization – Simulate external services dynamically rather than relying on static mocks.
Database Sandboxing – Use in-memory databases or isolated database instances for integration tests.
Controlled Environments – Deploy a dedicated test instance of external services with controlled test data.
These strategies balance realism and test efficiency, reducing the fragility often associated with excessive mocking.
Final Thoughts: TDD is a Tool, Not a Mandate
The conversation between DHH, Kent Beck, and Martin Fowler revealed that TDD is neither a silver bullet nor a broken methodology. Instead, it’s a tool with both strengths and weaknesses. Some developers thrive with TDD, while others prefer different strategies. What matters most is ensuring confidence in the code, whether through TDD, self-testing practices, or a hybrid approach.
The real question isn’t Is TDD Dead? but rather, Is TDD the right tool for your project? Engage in the discussion: How do you ensure your code works? Do you use TDD, or do you prefer other testing strategies?
Subscribe to my newsletter
Read articles from Juan Pablo Converso directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
