TDD – a paradigm shift within the software development process for the Agile Environment
Test-driven development is a mechanism for designing software. By using this development style, developers first gather requirements and then formulate and implement a set of executable tests that completely characterize those requirements, and finally those tests are executed, one by one, by writing the requisite target application software. Test-driven development (TDD), is also referred to as test-driven design, because it adheres to and works based only on the underlying concept “get something working now and perfect it later”. Test-driven development (TDD) is considered to be a novel approach to software engineering because it consists of short development iterations in which the test case(s) covering a new functionality are designed first and then the necessary implementation code to pass these tests is implemented afterwards. Finally the correctness of the code is verified by performing similar tests against the same test cases. Further, in the subsequent phases, the identified defects are fixed and components are refactored to accommodate changes. This process is iterated as many times as necessary until each unit is functioning according to the desired specifications.
Actually, the value of writing tests early in the design process was first mentioned in 1980 by Boris Beizer, a noted software testing expert. He described the benefit to the testing process of thinking about testing earlier, and then elaborated this point to include the idea that tests developed before the targets of the test may provide additional value in guiding the design effort. Kent Beck first explicitly articulated the concept of test-first programming in an article co-written with Erich Gamma called ‚Test Infected: Programmers Love Writing Tests.‚ The simple idea that Beck and Gamma conveyed was to “write a test that won’t run, then write the code that will make it run.”
TDD – Conceptualized
Test-driven development is an important Agile development practice which aims to set some concrete, detailed expectations in advance and allows those expectations of code behavior to guide the implementation. Further, it is important to acknowledge that test-driven development is not a one-way or linear process when compared to a traditional test-last process development, which proceeds from the analysis stage through to testing in a linear fashion, with a debugging phase following the initial design and implementation. However, test-driven development requires that developers request and obtain feedback from running tests throughout the development process, because running the tests frequently provides a loop-back flow of information from the developer to the system under development, and back again to the developer. So, the feedback that the developer obtains from running the tests will potentially increases the amount of information that is available to the developer, and eventually form the basis for a possible source of the developer’s greater understanding of the source code they are developing. The highly iterative nature of the process also accords developers the opportunity to ‚pass a test better‘, in other words to refactor their code using the passing test as a guide to their effort. Here, refactoring steps may include steps to improve the overall quality of the target source code or to improve the performance of the methods under test.
However, with both traditional test-last process development and TDD we are not actually intending to strive for perfection, instead we are testing to the importance of the system. To rephrase it from a testing standpoint, you should ‚test with a purpose‘, and know why you are testing something and to what level it needs to be tested. Nevertheless, an interesting side effect of TDD is that we tend to achieve 100% test coverage, i.e., every single line of code is tested thoroughly, which is something that the traditional test-last process doesnot guarantee, although it recommends it.
So, having deliberated on the above lines, and taking ScottW.Ambler’s findings on ‚Agile‘ into consideration, we can conclude by saying that test-driven development is typically a combination of test-first development and the refactoring process. The sections below outline this in detail.
TDD = TFD + REFACTORING
Test-first programming involves formulating (automated) unit tests for the code before you actually write the production code, i.e., instead of writing tests afterwards (or, more precisely, not even writing them at all), you always begin with a unit test. Importantly, for every small chunk of functionality in the code you would first build and run a small (ideally very small) focused test that specifies and validates what the corresponding code is intended to do. Technically, this test might not even compile at first and will fail, because not all of the supporting classes and methods/routines/procedures it requires may exist. At this point, it functions as a kind of executable specification and you then get it to compile with minimal production code so that you can run it and watch it fail again. Finally, you then produce exactly enough code to enable that test to pass. This exercise comprising a series of fail/pass situations helps you to have a better understanding of the code and its consistent behavior.
Actually, this technique appears to be very odd at first to quite a few groups of developers/programmers who actually try it, and eventually it makes them ask the questions: “Why all this trouble?” and “Surely it slows down progress considerably?” But then such questions only make sense if the programmers/developers end up relying heavily and repeatedly on formulating the same unit tests later. However, those who practice test-first regularly claim that those unit tests more than pay back the effort required to write them.
Agile teams who implement TFD often find that the closer the unit test coverage of their code is to some optimal number (somewhere between 75% and 85%, many teams find), the more agile their code is, which means it is easier for them to keep the defects in the code to very low levels and, therefore, easier for them to add features, make changes, and still deliver very low-defect code seamlessly every iteration.
Test-First Programming/Development vs. Debugging
It is very useful to compare the effort spent writing (unit) tests up front to the time spent on debugging. Usually, debugging involves looking through large amounts of code and takes more effort, which in one sense appears to dissipate, since debugging involves some considerable time investment in scaffolding exercise, setting up break points, monitoring temporary variables, having print statements in place, etc., that are all essentially discarded at the end. Importantly, once you find and fix the bug, all of that underlying analysis is essentially lost, i.e., if it is not lost entirely for the developer who developed the code, it is certainly lost to other programmers who maintain or extend that code in future. So, this exercise makes it difficult for analysts/architects and/or managers to predict how long debugging will actually take.
However, test-first work lets developer concentrate on a bite-sized chunk in which fewer things can go wrong, and with test-first in place, the tests are there for everybody to use forever, i.e., if a bug reappears, the same test that caught it once can be used to catch it again. If a bug pops up because there is no matching test, you can still write a test that captures it from then on. So, these thorough sets of automated unit tests serve as a kind of filter for detecting bugs. Further, good test-first teams find that they get substantially fewer defects throughout the system life cycle and spend much less time debugging. Importantly, well-written unit tests also serve as excellent design documentation that is always, by definition, in sync with the behavior/functioning of the code. Finally, if your code’s behavior is nailed down with lots of good unit tests, it is much safer for you to refactor the code. But if a refactoring (or a performance tweak, or any other change) introduces a bug, these tests would still be helpful in alerting the team quickly.
The difference between TDD and TFD
Having outlined the underlying objectives for each of the approaches, it is essential for us to understand the key differences between them. Firstly, TFD mandates that the test is written before the code so the code will always be testable, which seems to be more efficient than having to change the already developed code to make it testable. Further, TFD doesnot say anything about other activities in the development cycle such as requirements analysis and design, etc. Unlike TDD, the tests drive the design and the complete coding phase is split into two, whereby in the first sub-phase the focus is on meeting the requirements and in the second sub-phase the focus is on creating a good design.
Refactoring Technique – Conceptualized
Refactoring is actually a simple technique in which you make structural changes to the code in small, independent, and safe steps, and test the code after each of these steps just to ensure that you havenot changed the behavior – i.e. the code still works the same, but just looks different. Nevertheless, refactoring is intended to fillin some short-cuts, eliminate duplication and dead code, and help ensure the design and logic is very clear. Further, it is equally important to understand that, although refactoring is driven by certain good characteristics and shares some common attributes of debugging and/or optimization, etc., it is actually not the same because:
- Refactoring is not all about fixing any bugs.
- Again, optimization is not refactoring at all.
- Likewise, revisiting and/or tightening up error-handling code is not refactoring.
- Adding any defensive code is also not considered to be refactoring.
- Importantly, tweaking the code to make it more testable is also not refactoring.
The refactoring process generally consists of a number of distinct activities which are dealt with in chronological order:
- Firstly, identify where the software should be refactored, i.e., figure out the code smell areas in the software which might increase the risk of failures or bugs.
- Next, determine which refactoring(s) should be applied to the identified places based on the list identified.
- Guarantee that the applied refactoring preserves the behavior of the software. This is the crucial step whereby, based on the type of software, real-time, embedded and safety-critical measures, for example, have to be taken to preserve their behaviors prior to subjecting them to refactoring.
- Apply the appropriate refactoring technique.
- Assess the effect of refactoring on the quality characteristics of the software, e.g., complexity, understandability, maintainability, etc. Do the same for the process, e.g., productivity, cost and effort, etc.
- Ensure the requisite consistency is maintained between the refactored program code and other software artifacts.
Refactoring Steps – Application/System Perspective
The points below clearly summarize the important steps to be adhered to when refactoring an application:
- Firstly, formulate the unit test cases for the application/system. The unit test cases should be developed so they test the application behavior and ensure that this behavior remains intact even after every cycle of refactoring.
- Identify the approach to the task for refactoring – this includes two essential steps:
- Finding the problem – it is about identifying whether there is a code smell situation with the current piece of code and, if yes, then what the problem is all about.
- Assess/decompose the problem – after identifying the potential problem, assess it against the risks involved.
- Design a suitable solution – introspect what the result will be after subjecting the code to refactoring. Accordingly, formulate a solution that will be helpful in transitioning the code from the current state to the resultant state.
- Alter the code – now proceed with refactoring the code without changing the external behavior of the code.
- Test the refactored code – to ensure that the results and/or behavior are consistent. If the test fails, then rollback the changes made and repeat the refactoring in a different way.
- Continue the cycle with the aforementioned steps (1) to (5) until the problematic/current code moves to the resultant state.
So, when the refactoring technique is implemented in a disciplined fashion it adds the following key benefits, apart from what has already been achieved with TFD:
- Improves the overall software extendibility.
- Reduces and optimizes the code maintenance cost.
- Facilitates highly standardized and organized code.
- Ensures that the system architecture and design is improved internally by retaining the behavior.
- Guarantees three essential attributes: readability, understandability, and modularity of the code.
- Ensures constant improvement in the overall quality of the system.
Now, having both the techniques of TFD and refactoring in place, the test-driven development is said to be complete and is believed to lead to improved quality because of the extensive verification being performed by the teams during the course of the development phase. Importantly, there is also a perception stating there may be improved testability in the resulting designs and also improved extensibility, as the code/program evolves to match its requirements.
Nevertheless, one of the chief limitations of TDD is the fact that tests can sometimes be incorrectly formulated or applied, and this may result in units that do not perform as expected in the real world domain. So, even if all the units work perfectly in isolation and in all expected scenarios, end-users may encounter situations that may have not been imagined by the developers and/or testers. However, the final results of TDD are only as good as the tests that have been used, the thoroughness with which they have been executed, and the extent to which they resemble conditions encountered by users of the final product.
Key Benefits of TDD
From the development standpoint, listed below is the gist of some key benefits that can be achieved seamlessly by having TDD implemented in a disciplined fashion:
- Foremost, writing tests first requires you to really consider what you want from the code. So, this primarily helps in attaining a complete understanding of the requirements to be simulated and moving inline with the underlying purpose of the code.
- Promotes the creation of a detailed specification, as the resulting unittests are simple and act as good documentation for the code.
- Involves a short feedback loop, which reduces the time involved with reworks.
- Allows the design to evolve and adapt to the changing dynamics of the problem/requirements.
- Enforces radical simplification of the code, i.e., you will only write code in response to the requirements of the tests.