Test-Driven Development, or TDD, is one of the best practices of eXtreme Programming (XP). Test-driven development is a development method for software in which tests are written first and then the code. The name test-driven development is derived from Kent Beck, who put this technique on paper in 2002 and is credited for rediscovering the technique [Beck 2002]. The original description of TDD was written by D.D. McCracken in 1957 [McCracken 1957].
TDD has a lot of impact on the way in which development testing is organised these days. It is an iterative and incremental method of software development, in which no code is written before automated tests have been written for that code. The aim of TDD is to achieve fast feedback on the quality of the unit.
TDD is a design method. Design is superadditive (the whole is greater than the sum of its parts) and Test Driven Development (TDD) is about designing a solution to a problem.
One of the biggest problems with software development, is not knowing what will happen, when things interact. Our code might be used in scenarios we didn’t expect, and therefore our code might not be able to handle it. Applying TDD helps in identifying these unsuspected interactions.
TDD Process: The three laws of TDD
[Reference: Uncle Bob]
- 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.
At first glance these 3 laws might seem very unproductive, since it will require the developer to switch between writing unit-tests and code. But there are many benefits of doing that, which we will make clear in the following section.
The development cycle below is based on the book of Kent Beck. This cycle can be repeated as many times as needed to make the code fully functional according to the requirements.
- Writing a unit-test without writing any code, will make the unit-test fail (RED)
- Writing the code to make the unit-test pass, will make the unit-test pass (GREEN)
- Updating the code, while the unit-tests still passes, will not break the code. (REFACTOR)
Creating a test
- Write a test
TDD always begins with the writing of test code that checks a particular property of the unit based on a requirement, and then the code is written.
- Run all tests and see if the new test fails
This test should initially fail, as the piece of code to which this test applies does not yet exist. The failure of the new test is important to determine whether the test is effective (contains no errors and therefore always succeeds).
Encode and test
- Write the code
Write the minimum code required to ensure that the test succeeds. The new code, written at this stage, will not be perfect. This code may only contain functionality that is necessary for the test to succeed. This is acceptable for the code does not have to be written perfectly, as long as it works. It is important that the written code is only designed to allow the test to succeed; no code should be added for which no test has been designed. In the subsequent steps the code will improve on it and rewritten according to the standard.
- Run tests and see if they pass
In this step, all tests, new and all the previously created tests are run again to see if all tests pass. If this is the case, this will be a good starting point for the next step. The developer knows that the code meets the requirements included in the test.
- Rewrite the code
The code is cleaned up and structured without changing the semantics. Also, any duplicate code necessary to minimize the interdependence of the components will be removed. The developer regularly executes all the test cases. As long as these are successful the developer knows that his amendments are not damaging any existing functionality. If the result of a test case is negative, he has made a wrong change.
The application of TDD has a number of big advantages. It leads to:
- More testable software
The code can usually be directly related to a test. In addition, the automated tests can be repeated as often as necessary.
- A collection of tests that grows in step with the software
The testware is always up to date because it is linked one-to-one with the software.
- High level of test coverage
Each piece of code is covered by a test case.
- More adjustable software
Executing the tests frequently and automatically provides the developer with very fast feedback as to whether an adjustment has been implemented successfully or not.
- Higher quality of the code
The higher test coverage and frequent repeats of the test ensure that the software contains fewer defects on transfer.
- Up-to-date documentation in the form of tests
The tests make it clear what the result of the code should be, so that later maintenance is made considerably easier.
The theory of TDD sounds simple, but working in accordance with the TDD principles demands great discipline, as it is easy to backslide: writing functional code without having written a new test in advance. One way of preventing this is the combining of TDD and Pair Programming; the developers then keep each other alert.
TDD strategy for testing GUI
TDD also has its limitations. One area where automated unit tests are difficult to implement is the Graphical User Interfaces (GUI). However, in order to make use of the advantages of TDD, the following strategy may be adopted:
- Divide the code as far as possible into components that can be built, tested and implemented separately.
- Keep the major part of the functionality (business logic) out of the GUI context. Let the GUI code be a thin layer on top of the stringently tested TDD code.
TDD has the following conditions:
- A different way of thinking: many developers assume that the extra effort in writing automated tests is disproportionate to the advantages it brings. It has been seen in practice that the extra time involved in test-driven development is more than made up for by the gains in respect of debugging, changing the software and regression testing.
- A tool or test harness for creating automated tests. Test harnesses are available for most programming languages (such as JUnit for Java and NUnit for C#).
- A development environment that supports short-cycle test-code-refactoring.
- Management commitment to giving the developers enough time and opportunity to gain experience with TDD.
TDD and Specification and Example (SaE)
TDD can be amplified by Specification and Example (SaE) approaches, for example Acceptance Test Driven Development (ATDD) and Behavior Driven Development (BDD). The difference between ATDD and BDD is in the area that ATDD scenarios focus on the “what” question and BDD gives substance to the “how” question. These two also complement each other.
The figure shows how SaE approaches interact with TDD. SaE is shown on the left and TDD on the right.
Looking at the Agile testing quadrants, one sees that SaE and TDD apply to the left side of the quadrants. SaE is Business facing and involves the business analyst (BA) and testing roles. TDD is Technology focusing and fits within a Developer role, i.e. part of creating code. However, both have the goal of “Guiding the team”, in other words to help the team building the right system.
From a BA perspective, ATDD and BDD scenarios can be created which serve as input for performing Development tasks. The value of SaE scenarios in combination with TDD (and unit tests/component tests) is that they cover scenarios that comprise two or more units. A higher coverage at unit level can be pursued combined with a limited number of SaE scenarios. Consider, for example, a business process in which two sub-processes run parallel to each other. The separate sub-processes can be tested at unit level, but the interaction between the two can be covered at BDD and ATDD level.
By combining the different approaches, we can generate an effective and balanced set of scenarios. These scenarios can additionally be identified using test design techniques. Consider, for example, performing a boundary value analysis.