Test-driven development is a process that assists in creating high quality, well designed, loosely coupled and maintainable code that can be refactored with confidence. The process relies on writing unit tests before creating the code that they validate.
What is Test-Driven Development?
Some development practices and methodologies lead to code that grows in complexity over time, with a corresponding decrease in maintainability. You may find that some projects are hard to modify, as adding new features might break existing functionality or cause subtle bugs that are difficult to rectify without introducing further defects. When you encounter such projects it may be difficult to incorporate small changes. Large refactoring operations can be near impossible.
If a project includes a high level of unit test coverage, meaning that most or all of the source code has tests to validate their functionality, it is likely to be easier to maintain. The key reason for this is that, in order to perform high quality testing, the code must be loosely coupled and is more likely to have a better design. In addition, you can confidently make changes to code that has a full set of tests, knowing that those tests will fail if you introduce errors.
Test-driven development (TDD) is an iterative development process that aims to achieve high standards of design, excellent testability of source code and high test coverage for your projects. As the name suggests, the process is driven by the creation of automated unit tests. The name is slightly unfortunate, as it suggests that the sole goal of TDD is test coverage. In reality, a key benefit of TDD is improved design. These leads some practitioners to call it test-driven design, or other names that remove the word, "test" completely.
The TDD process uses very short iterations, each adding a tiny amount of functionality to your software. Each iteration has three phases, described by the TDD mantra, "Red, Green, Refactor". The three phases are:
- Red. The colour red refers to the colour shown in many automated test runners when a test fails. The idea is that before you write any production code you should write a failing test for that functionality. This often means writing test code that exercises classes or methods that have yet to be created. It is very important that every new test fails. New tests that pass immediately suggest that either the code they test is already present or that the test itself is defective. New tests should be small, aiming to test only a single aspect of the software being developed.
- Green. The second phase of each iteration is writing code to make the new test pass. The smallest amount of code possible to make the test pass, without causing any existing tests to fail, is added to the project. This may mean creating dummy return values. For example, when creating a method that adds two values, the first test may check that adding two and three returns five. To make this test pass the method could return the fixed value of five, rather than performing an addition. Later tests will ensure that the method works correctly in all situations.
- Refactor. A very important, though often overlooked, phase is refactoring. After creating a new test and making it pass you should consider the design of the code. Sometimes no refactoring is required. Other times you might change method or property names, or extract methods for a cleaner design. In some cases you may perform major refactoring of the code for an improved design. This is made easier because the existing tests can be run to ensure no bugs are introduced during the changes.
On completion of an iteration you simply start the process again. Usually an iteration is over in minutes, so most of the time you have working, if incomplete, code.
Dealing with Bugs
Although defect rates can be lower when using TDD, you will still create bugs. These should be corrected with the same three-phase iterations. First you create one or more tests that demonstrate the bug by validating the correct behaviour. These tests should fail. Next you fix the code to make the test pass, performing regression testing by executing all of the tests. Once the bug is fixed, you should perform any appropriate refactoring to keep the code clean and readable.
TDD provides a number of benefits. Some of these are:
- Writing the tests first forces you to consider how your classes will be used and how methods and properties will be called before you start coding them. As you must create unit tests for all functions you will generally be forced into a loosely coupled design because of the use of injected test stubs and mock objects. To enable the use of these test doubles you are likely to work in accordance with the dependency inversion principle.
- On completion of any iteration you will have a working, if incomplete, piece of software with a full set of regression tests. These tests allow you to recognise bugs quickly should they be introduced later. In most circumstances it is less costly to rectify a problem if it is identified earlier. Having full test coverage also allows you to make major refactoring changes to your code with confidence, as you can quickly find any problems that are introduced. Debugging tends to be easier because the iterations are so small and little changes between the creation of the individual tests. This means you spend less time stepping through code in the debugger.
- Some studies have shown that adherence to TDD can make developers more productive. For very small projects the overhead of testing may lower productivity. As the size and complexity of a project grows, the lower requirements for debugging exercises may give an advantage.
- Well organised tests provide a level of documentation for your software. By looking at the tests that cover a particular method you can see how the method is used quickly and, if the tests are well named, see the reasoning behind coding decisions. It should be noted that this should not be the only technical documentation for your software.
19 November 2012