As we were sharing in an article in Today Software Magazine (Adrian Bolboaca & Alexandru Bolboaca) our views about software craftsmanship, we came to the fact that Test Driven Development is one of the core practices for software craftsmen. Besides the increasing number of articles, blogs, short movies or books on this subject, Test Driven Development (TDD) continues to generate confusion in the programmer communities. This article tries to structure and clarify the subject and to offer support to those willing to learn more about it.
The classical description of TDD is that the programmer:
- Writes only one automated test that fails (often called the “Red” step)
- Does the smallest change in the code to make the test pass (“Green” step)
- Refactors the code (“Refactor” step)
This cycle repeats with a high frequency, up to 5 minutes for experienced practitioners. For beginners, 15-30 min is an expected duration, and decreases with experience. We do not talk here about writing tests on existing code; in this case the time needed to write tests depends on the code complexity.
This description is very easy to show, but unfortunately misses essential details for those that want to start applying TDD, thus creating confusion.
The first thing we need to understand about TDD is that, contrary to its name (development lead by tests), it is not a method of testing. TDD is a method to design the solution for a problem. The tests serve two purposes:
- advancing the design (the solution)
- verifying that the changes in the code did not affect the problem solution created until the tests ran.
Because software design is an ambiguous term, it’s worth explaining what is design in this context.
Design is not anything else but “creating artefacts that solve problems” (“Design: Creation of Artifacts in Society” by Karl T. Ulrich). For programming, the artefact is the code. More exactly, at the bottom level, the created artefacts are the variables, methods, classes, parameter lists and the collaboration between objects (called as well “contracts”).
Two things are important for the design of a software application. It:
- solves a problem…
- in the simplest and most elegant way possible
If programmers write elegant code and do not solve a problem, then there’s no solution and therefore no design. The fastest and most elegant way of demonstrating that a problem is solved is to run automated tests that prove the solution. Of course, these tests need to be reviewed against the problem, to make sure all cases were covered.
Finding some simple and elegant solutions hits a couple of obstacles:
- Incomplete understanding of the problem.The human brain has a limited capacity, and the problems programmers need to solve increase in complexity. It’s not a surprise that even the best programmer make mistakes.
- Premature generalization (as other “cognitive biases”). Often programmers want to create generic solutions before having enough specific cases that justify them. Taken to extreme, this tendency can create an apparently well conceived code that is very difficult to maintain.
- The tendency to use known solutions. “When you have a hammer, everything you see are nails”, as the old saying goes. The problems the programmers solve each day can seem, at a superficial level, very similar. The reality is that in programming solutions depend a lot on small details.
- Fast requirements change. It is already well-known that requirements change often. One solution that was good yesterday might not fit today any more.
The realities from the day-to-day life of a programmer lead to the need of easy to change designs. Requirements change, teams change, programmers learn more each day about the product and technologies. These days, good design is almost synonymous with easy to change design. This is why the qualities of a good design are, in 90+% of the cases:
- Both the structure and the code are easy to understand for all the programmers involved
- Most new functionalities require minimal changes in the code. This is possible when small and very specialized classes work together by following well-defined contracts between their interfaces.
- It is easy to check that the changes in the code did not affect the existing implementation.
A solution for these problems is incremental design. Incremental design means creating the design while writing the code. Incremental design is an alternative to the classic way of doing design: before starting writing code, on paper or by using special tools.
To do incremental design one needs to follow the steps:
- Analyse the problem and divide it in smaller problems. For example, in the case of creating a Tetris game, you can start with the simplest game possible: one piece of the size of a square that falls into a well with height 1 and the game ends (alternatively, one can consider that the square filled a row that disappears, or the rule of eliminating the row will be easily added later).
- Identify concrete examples (expected values of input and output). For example, after the game begins, the piece that appears on a board into a certain place.
- Implement one example in the simplest way possible.
Incremental design fights the problems mentioned earlier in the following way:
- By defining examples, the problem becomes clearer. Even more, the examples can be discussed easier with non-technical persons (customers) than the code.
- The list of examples is filled-in as new behaviours are identified during development. By iterating, the chances of forgetting parts of the problem decrease.
- Simplifying the problem lets diminishing the complexity so that our brain can manage it.
- Implementing the simplest solution for every example leads to avoiding premature generalization.
- The solution is always simple (as much as the problem lets it), which makes easier to change it in the case of requirements change.
Test Driven Development is the best known method to do incremental design.The tests code examples of usage of the solution and continuously verify the solution. Implementing the simplest solution at every moment helps premature generalization. Refactoring leads to simplifying the solution.
Because developing using TDD starts from examples, and the code written at every step is as simple as possible and without generalizations, the refactoring step implies especially identifying and reducing similarities from the code (also named “duplication”, because two chunks of code do the same thing in different ways). The similarities are eliminated by generalization, leading to abstractions in code. (No, it is not about abstract classes, but about classes that serve for a larger purpose that they were initially made for).
By introducing abstractions, the programmers obtains a flexible design, perfectly adapted to the current problem. Because of using automated tests, the programmer can show any time that his solution is perfectly valid for the list of behaviours defined by tests. Sounds excellent, right?
But adopting TDD is not simple. Here are some obstacles you might meet at the personal level:
- Learning necessary techniques to write simple unit tests. The test doubles (most important stubs and mocks) are very useful.
- Simplify. This is an ability that evolves in time, by exercise and with feedback from other people.
- Identifying similarities from the code. Some similarities are obvious, but others are more subtle and become visible with experience.
- Design. Similarities can be minimized by several ways, and some of them lead to better results than the others. The knowledge of software design (design patterns, design principles from which we mention S.O.L.I.D. Principles and the four elements of simple design) are essential to create the simplest solutions.
- Focusing more on the problem and less on the solution. School teaches programmers to think about solutions. Incremental design asks for examples and problem simplification before writing the first line of code. The programmer that tries TDD needs to push back the tendency to think about design or about code at this first stage.
- Fast and efficient refactoring. Refactoring can take a lot of time if not well mastered. Practising the refactoring techniques with the purpose to increase the speed is very important when using these techniques in production.
At the level of one team, even more, some time is needed for finding a common style for design and tests. For more complex environments (a lot of existing code without automated tests, more teams working at the same product, remote working, etc), the adoption needs great care to avoid productivity problems. Complex situations require technical coaching to manage the changes.
Incremental design means creating the design at the same time with the code. Incremental design is an alternative to the up-front design made on paper or in specialized tools. Incremental design starts from examples and generalizes the solution while proof, code similarities, appears.
This is the advantage of incremental design: it is proof-based instead of intuition-based. Generic solutions following this process are the simplest for the given problem, as much as it is known at that time.
TDD is the best known method of doing incremental design. TDD practitioners codify examples using tests that are kept afterwards to as validation for the complete solution. By identifying and minimizing similarities from the code during the refactoring step, the design gets simples and improves continuously.
The two main purposes of incremental design are simplicity and changeability. TDD and incremental design give a boost of efficiency especially for problems that are completely new and which are not mastered well by programmers.
Related articles on our blog you could find useful are:
How an unit test should look like
How unit test saved my holiday
5 Common Unit Testing Problems
From Design to Design Principles
Also, our services on technical trainings might raise you interest: