Test-Driven Development (TDD) is an evolutionary and iterative method that is an alternative approach to software development. Whereas in traditional approaches tests are written retrospectively, after the code they are designed to test, TDD reverses this approach by first writing the tests and then the code intended to pass them.
A potentially useful metaphor for helping us to understand the TDD process is to look at how, in the natural world, DNA ‘codes’ for an organism’s functions (albeit indirectly, through protein structures). Let’s say, for example, that we have been asked to engineer a new organism capable of surviving in a particular habitat. Just as software code needs to be written in a way that allows it to pass the process of software testing, DNA ‘codes’ for physical characteristics that create behaviours that allow an organism to pass the tests a particular habitat might throw its way.
Let’s go to work…
Let’s begin the task of designing our new organism. We can begin to think about creating our organism by generating its DNA sequences, which create the genotype (the rough equivalent of ‘functions’ in the world of software programming) that will shape its phenotype (its observable characteristics) and thus its behaviours in the designated habitat. Of course, once we have the first version of our organism, we can’t throw the poor, unfinished and untested thing into the wild without properly working out if it has a chance of surviving. In software terms, this would be the equivalent of launching a software product that has not been fully tested on the market, carrying a high risk of failure and a strong chance of damaging the reputation of the company responsible. Because we have been given the role of almighty creator, we can create a private zoological park, designed to replicate the conditions our organism will find waiting for it in the ‘real world’. This private zoo can be seen as the equivalent of a software test bed, designed to test the suitability of the DNA sequence, just as we are able to test software code before it is used in the real world. Once we have created our zoo, we place our organism in this controlled environment to see what happens. If the organism survives, the test was successful and we can release the organism into the wild. In the world of software, our product is ready for shipping. If our poor organism perishes, we need to go back to the drawing board to refine and improve its design before giving it another go.
Using the TDD approach to develop the organism is all about first creating the test ‘zoo’, which we hope will accurately reflect the real-world environment that our organism will hopefully survive in. When our DNA sequence leads to an organism that fails in this test environment, we do not release it into the wild, as clearly, that sequence isn’t capable of creating an organism that can survive that habitat. Instead, we refine things. We examine the DNA sequence and systematically re-design it in a way we think will lead to an organism that is capable of surviving in our test environment. The aim is not to over-engineer our organism but to create a DNA sequence just good enough for the organism to survive. At each increment, we place our organism back in the zoo to see how the latest version of the DNA performs, until our organism is finally able to survive. Once we have a successful DNA sequence – our organism survives – we might want to re-examine the DNA to ensure it is ‘clean’, getting rid of redundant or unnecessary junk. In the world of software, this roughly corresponds to ‘re-factoring’, where the internal structure of the code is changed without affecting its external behaviour. We might also consider adding more conditions to our test environment, such as extreme environmental conditions like floods or high temperatures, a virus or other organisms. We repeat our tests until we are happy that our organism has managed to survive all its ordeals. At this point, it’s time to release it into the wild, safe in the knowledge that our organism is designed to survive.
The TDD re-evolution
Now, let’s step away from the natural world and immerse ourselves in the world of software. TDD relies on repeating a short and simple development cycle:
- Add test: before any code is written, a test is created that takes into account all the possible inputs, outputs and error conditions.
- Run test: the test is run for the first time. It will probably fail. In the first instance, this is simply because no code yet exists to satisfy the test.
- Write code: the code is written, or improved to overcome any identified failings. This process can be repeated until the test is passed.
- Re-factor: once the test is passed, the code can be cleaned, or ‘re-factored’. As long as the code continues to pass the test, the code works. This allows the code to be improved while eliminating concerns that any changes might introduce bugs.
- Repeat: the whole process is repeated.
To TDD or not to TDD?
We have already discussed how TDD allows us to test code without having any prior knowledge of it that would prejudice the creation of the test. There are also further advantages that help to address some of the most common excuses for not testing software properly:
- “It’s a waste of time and effort to test, since there’s only a slight change in the code.”
- “There’s not enough time to implement further changes. The project manager wants to move the code to production as soon as possible, as we’re close to the deadline.”
With TDD, any change in the code that would have implications on the software’s external ‘behaviour’ or function, is guaranteed to be tested, because the test exists before the code is written, thus avoiding situations where the coding is complete but we are too close to the shipping deadline to test the software properly. Using TDD does mean writing more tests but it ensures that all the code is covered by at least one test. Having more tests also helps reduce the number of code defects.
TDD can also help drive the overall design of the software. Because TDD focuses on creating the tests first, it can be a useful way of ensuring subsequent coding places the function being tested at the heart of the software. A further advantage to using TDD is that it allows programmers to take small, incremental steps in evolving code. Consider, for example, the case where a new software function is required. A few new lines of code are added and the software is tested again. If the software fails the test, we will know this is likely because of the introduction of new lines of code, making problematic code much easier to find than if we are trying to look for the proverbial needle in the haystack, looking through thousands of lines of code in the complete piece of software. The implication is that the faster the compiler and regression test tools, the more attractive it becomes to proceed in smaller and smaller steps. In other words, the better the performance of the development environment, the less time spent on re-running the test each time small changes are made to the code.
Finally, TDD can also lead to code that is more modular and flexible. TDD requires developers to think about the software in smaller units that can be implemented and tested independently before they are integrated. This leads to cleaner, more versatile and more flexible software.
So, should we all start using TDD right away without a moment’s hesitation? Not necessarily, no! Although, theoretically, TDD is scalable, in reality it can be an issue. What’s more, some developers lack testing experience and badly-written tests can be expensive to maintain. Another problem can arise when tests created in a TDD context are written by the developer who writes the code. This means that both tests and code could share the same blind spots. Or a developer might misinterpret the design requirements, meaning both the code and the tests might be wrong, producing misleading results. Furthermore, because of TDD’s reliance on unit tests, some problems might arise not from faults within the units themselves but rather from their interactions when they function as part of a bigger system.
However, there’s no doubt that TDD has real merit in the world of software testing. It avoids clouding the tester’s mind with code that already exists and tests can focus specifically on the system functions required. It also allows for complete test coverage of the code and helps to eliminate any code that doesn’t have functional value. Ultimately, no software testing approach offers a ‘one size fits all’ solution but TDD is another useful tool in the quest for more efficient and effective testing methods.
Safety-Critical Validation - White Paper
Embedded software is now being used in to perform an increasing number of safety-critical functions. As software complexity and use increases, the chance of error also increases, so software systems deployed in safety-critical applications must satisfy rigorous development and verification standards. Learn more in this latest document from our experts.