Every once in a while, I have conversations with people about what really is TDD. Since I built a certain knowledge on the topic in time not only by using it but also by explaining it to others, I decided to write this article that details my definite view on what TDD is. I hope you’ll find it useful.
This is a long article. If you’re in a hurry, this is the 5 minutes version:
- Design is intentionally conceiving and giving form to artifacts that solve problems
- Computer code is such an artifact, therefore any piece of code that intentionally solves a problem is designed
- Therefore TDD is a method for obtaining design
- Good design means design that has certain qualities. The most common quality we seek today is changeability.
- TDD offers some built-in qualities: testability and improved mistake-proofing. The developer has to work to improve other qualities such as changeability. This is why practitioners use SOLID principles to guide their design decisions.
- Therefore the qualities of the design obtained through TDD largely depend on the skills of the designer
- When doing TDD, the developer designs before starting (eg. because using an MVC web framework) and all throughout the TDD cycles: when writing the test (pick class / method names, decide on types of classes to use etc), when implementing the code (variable names) and when refactoring.
- I propose that TDD is a method for incremental design, since the solution grows step by step. This also relates to problem solving, and the circle closes – because design means solving a problem.
Interested? There’s much more!
1. Design means to create an artifact that solves a problem
For some reason, the term “design” has become overloaded and confusing. Let’s take for a moment the example of a smartphone. What defines its design? How it looks? How it acts? The materials that compose it? Were the older mobile phones “designed” or only the newer, slicker smartphones are “designed”?
It gets more complicated when we talk about software. Is a piece of code “designed” when it follows SOLID principles? Is it “designed” when it’s procedural code? Is it “designed” when it has long methods?
I started asking these questions a few years ago because I had no idea about the answer. The only way to find answers was to learn more about design in other domains than software. After all, design is a discipline that’s been around for hundreds of years before our industry.
I finally found a definite answer that is satisfactory:
Design is conceiving and giving form to artifacts that solve problems artifact [is]… any product of intentional creation including … software
Therefore, any piece of code that solves a problem in an intentional way is design. Older phones were “designed” because they were solving a problem: having phone conversations with other people.
But if design is any piece of code that intentionally solves a problem, why do we use SOLID principles or clean code or other things? Probably for the same reasons that graphical designers use principles such as alignment or emphasis. To make design better.
2. Good Design Is Design With Certain Qualities
The most difficult thing to understand about design is that design has certain qualities. For example, one of the qualities of user experience design is “usability” – how easy it is to use the application and how fast a user can solve its problem with it.
These qualities are contextual. For example, a mobile user experience is different from a web user experience. The medium matters in this case.
What about software design? What are some of its qualities? Here’s a quick list from the top of my head.
Static qualities (when the code is not running)
- Readability: the code can be easily read
- Navigability: the code is easy to navigate
- Reasoning: by reading the code, you can easily infer how it behaves at runtime
- Changeability: it is fast to change the code when fixing a bug or adding a feature
Dynamic qualities (valid at runtime)
- Disaster resistance
- Mistake proof: it is difficult to make a mistake when changing the code
The qualities of software design are contextual too. In case of a typical web application, changeability and performance are typically the most important, with scalability and security coming close. For mobile applications, performance and changeability are important. For high volume data-driven web services, performance and scalability are key. And so on.
One thing is constant: changeability is important in about 90% of the applications we build today. We have to change them often, so it’s useful if we can change them faster.
Given all that, what is good design? Should be easy:
Good design is a piece of code that intentionally solves a problem and that exhibits the design qualities necessary in the given context.
The reason we keep talking about things like the 4 Principles of Simple Design, SOLID Principles and Clean Code is because we need one quality that these principles offer: changeability. Having tests helps us avoid mistakes when changing the code. SOLID Principles are all about the ease of changing code. Duplication prevents us from making changes fast. Bad names prevent us from understanding code, just making it more difficult to change it.
It’s important to note however that this is not the only design quality we should watch for. Technologies like nodejs or vertx favor performance over changeability. Stored procedures or views for creating reports have the same effect.
Balancing design qualities is one of the most difficult things for a programmer. Changeability is however a good start, being such a wide spread concern.
If we understand what design and good design are, what is really TDD?
3. TDD Is A Method To Design
If we look at the definition of design above, it’s obvious that TDD is a method to design. We write code to solve a problem and we use tests to define the expected behavior of the solution. When the tests pass, another bit of the problem gets solved. We’ve just created intentionally a piece of code that solves a problem. This is design.
But is it good design? This is an interesting discussion. To answer it, we should look at the design qualities that TDD forces us to build. There are two:
- Testability – obviously, since the code is tested
- Mistake-proofing – there’s a lower probability of introducing mistakes when changing code that’s already tested. The developer can still introduce mistakes if the problem was incorrectly understood or if the tests are incorrect.
Is this enough to call the result of TDD “good design”? Most likely, the answer is no. It makes complete sense to say that:
The quality of the design depends on the designer’s skills
TDD by itself will not produce good design. It does produce design with certain qualities, but it’s up to the developer to take it further.
4. When Do We Design While Doing TDD?
It’s now time to introduce yet another aspect of software design. Writing the code is not enough; what’s important is to structure the code in a certain way. The least structuring we can do is to write all the code in one huge method. The computer won’t care, but it will affect the qualities of design.
When doing TDD, a programmer has to make a lot of decisions regarding the code structuring:
- use a class, a method, a variable, a member etc.
- how to name things
- how the classes collaborate
- what data types to use
These are all design decisions that contribute to its qualities. Naming a variable ‘a’ will make it less readable than naming it ‘amountOfMoney’. Adding 10 layers will make the code more difficult to understand than using just two layers. A class with 10 collaborators is more difficult to understand than a class with 3 collaborators.
When do we make these decisions? Always! More concretely:
- Upfront. For example, using an MVC web framework means already following constraints for the code structure.
- When writing the test: the name of the class under test
- When writing the code: the names of private methods, instance members or local variables
- When refactoring: extracting a method, extracting a class or just renaming something.
Improving the qualities of design is a continuous process. It is not limited to the refactoring step or to the TDD cycle. And that’s ok, because actually…
5. TDD Is a Method For Incremental Design
Incremental means that we are designing software one bit at a time. We design a little before starting the cycles, a little while writing the test and a little when refactoring. The act of designing is intentional: we try to improve the qualities of design that are relevant in our context (typically changeability). Incremental also means that the solution grows step by step by slicing the problem into smaller problems. For example, to solve a problem that has as input a list of many numbers, we start from an empty list, then a list with one number and so on.
This process is very similar to another one: general problem solving. The circle just closed: since design means solving problems, and solving problems is best done incrementally, then what better way to call TDD than a way to incrementally design software?
The more knowledgeable readers will probably remember that TDD was often discussed in the context of “emergent design”. I think the name “emergent” has a problem: many developers I met tend to think that “emergent design” means design that appears out of nowhere because of a process. Of course, this is incorrect, but it leads to misunderstandings. I favor the term “incremental” as a better description of the process.
And There’s More …
This article focused on the most common use of TDD. Things are a bit more complicated because there is a suprising number of ways of using tests and TDD. Here are some advanced examples, by no means a complete list.
I have used TDD in the past to explore design alternatives: I solve the same problem using different constraints and compare the resulted designs. This is a more complex design method, since exploring alternatives is part of the general design process.
TDD can be used to learn new things. I have successfully used TDD in the past to teach programming languages to people without a programming background.
TDD can be used to explore the problem space as well. I do this by solving a part of the problem and then think how could the problem evolve. For example, if we take Conway’s Game of Life I asked myself: what if the universe changes? What if the time changes? What if the rules change? etc. I often do the same exercise with the business applications I’m building. I sometimes use TDD to explore the potential changes to the features.
In a different category, TDD As If You Meant It is an exercise based on TDD but with additional constraints that delay all the code structuring decisions for as long as possible. It is probably the most incremental approach possible, but it can also lead to code that doesn’t do much even after 1-2 hours. I find it an intriguing exercise but never use it in production.
Your questions and comments are welcome, please don’t hesitate to write them below.
“Technologies like nodejs or vertx favor performance over changeability”
I’m interested in the reasoning behind this assertion, my experience has been the complete opposite.
This is interesting. I would love to exchange impressions about this.
Here’s what I noticed from my limited experience with nodejs and vert.x:
Asynchronous code is harder to read than synchronous code.
It is harder for me to reason about the runtime behavior of the code when reading nodejs or vert.x code
I’ve seen a type of design that takes the request and passes it through a set of transformations until the response is generated. A change in one of the transformation can produce effects in the final transformation.
Do you have examples of nodejs / vert.x code that is easy to change? I would love to see some samples, and learn how to do this better.