The TDD Debate: Testing, Design or Development tool?
Test-Driven Development controversy: which role does it serve best?
Misconceptions
Not long ago, I received some criticism on one of my LinkedIn posts, with people claiming that Test-Driven Development (TDD) is not a design approach. My consequent answer post sparked a fierce debate, attracting both lovers and haters.
This made me think critically about TDD as a design methodology. So I decided to collect all the design aspects of TDD. In this post, I will detail the characteristics of designing production code. In a future post, I will explore the impact of TDD on designing quality tests.
The sad reality is that most developers don’t know what TDD is. The most common misconceptions are:
TDD is equal to testing
it’s about writing a big test first, then write production code, and repeat in cycles
it’s about writing all tests first, then all the production code
it’s a development technique and not a design approach.
So what is really TDD about?
The three forms of TDD
Let’s see what the incredible Allen Holub thinks about it:
Or Vaughn Vernon, a leading expert in Domain Driven Design:
First of all, we can’t really say that TDD isn’t a testing approach. We are literally producing code by continuously writing tests for our application. The TDD flow leads to a test suite that covers all the business logic of our system. However, TDD alone is not enough for testing, and you need additional practices such as:
mutation testing
property-based testing
integration testing
load testing
penetration testing
exploratory testing
and most importantly, beta testing with our customers.
We can’t really say that TDD isn’t a design technique either. TDD is an act of abstracting and designing code through the entire cycle. Software design happens in all the phases of the Red-Green-Refactor flow. TDD helps us design for simplicity, correctness, good user experience, and reliable tests.
Finally, we can’t really say that TDD isn’t a development approach. It provides a development framework with its cycle and three laws. It helps work in short cycles, manage complexity, and enable writing code in iterations. Its framework constrains us to take baby steps and change only one thing at a time, resulting in high confidence in every step we make.
Here are the three laws of TDD that ensure a strict development flow:
You are not allowed to write any production code unless it is to make a failing unit test pass
You are not allowed to write any more of a unit test than is sufficient to fail, and compilation failures are failures
You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
What is TDD then?
TDD is a blend of all three: design, development, and testing. It’s an all-in-one methodology that enables developers to design simple, clean, and tested code. It’s a way to incrementally develop our software and comprehensively test all our business logic. It’s the gold standard way of producing quality software, incorporating best coding and design practices.
Design through Red-Green-Refactor.
TDD helps both designing production and test code. Effective software design is integral to every phase of the Red-Green-Refactor cycle, ensuring that code is developed and tested in a structured and intentional way. Here's the image of TDD's design aspects for writing production code:
Red phase
In this phase, we always start with a failing test that is used to design the external view of our code. Our aim is to design a good user experience for our public APIs and meaningful interactions between our system collaborators. As the tests are the first user of the code, we test and design the API user experience before even implementing it. By doing so we can avoid expensive mistakes.
A failing test helps understand how the code will be used. It makes us think about the interactions with other modules. When you start with a failing test, you evaluate the usability of your code. You verify if the API namings, signature, and side-effects are meaningful and clean enough. You check if those concerns make sense from the user's point of view. TDD shifts our focus from implementation details to high-level business behaviors, letting us focus on what really matters: producing useful software that makes our customers happy.
TDD ensures testability by design. Because of the test-first principle, we are forced to have a design that is testable. Otherwise, we wouldn’t be able to write any failing tests for a code that isn’t testable. To make a behavior testable first we need to define its public API and expectations in a test. We make a contract describing what we are testing and what the expected behaviors are. Then by making the test pass, we fulfill the contract, resulting in a testable code. There is nothing that guarantees testable code when following test-last development.
Green phase
Here we design for simplicity. Simplicity is one of the most important traits of quality software. Simplicity is the art of maximizing the amount of work not done. In this phase, we are forced to write exactly the code we need. The third law of TDD is the key to unlocking simple design:
You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
We write the most minimal production code to fulfill the requirement expressed in our test. No unnecessary loggings, overengineering, or complex logic. We strive for minimalist code instead of a gold-plated solution. We go from the Red to the Green phase in the simplest way possible.
Next to simplicity, TDD ensures correctness. We can have the most elegant code but if it doesn’t do what you expect from it, it’s just a valueless piece of code. No code is better than incorrect code. The code written with TDD does exactly what we want. It behaves according to the expectations specified in our tests. A failing test makes sure that our expectation is meaningful. A passing test means that our code is working. And finally, refactoring ensures that our design becomes simple.
Refactor phase
TDD is primarily about growing the design through continuous refactoring. Most of our design actions will happen in this phase. We develop the design by practicing aggressive refactoring. Here we care about lower-level design concerns such as:
naming our software elements
abstracting and encapsulating business logic
removing duplication
and applying other design principles.
Many misconceptions about TDD are usually derived from the nature of the Refactor phase. TDD won’t tell us how to name our software elements or which patterns they should use in a given problem. It will just provide us with a framework to support our actions to be executed in a disciplined way.
Conclusions
Ultimately, the names we use for a technique are irrelevant. TDD is just a means to an end: writing quality code that works. To achieve this, TDD is what works best for me. However, I am happy to ditch TDD if I learned something more efficient. But as long as TDD remains the best way for producing quality software, I will continue to use it to Craft Better Software.
Great read
Amazing Post!