Test Driven Development
What is TDD?
In the traditional model of software development developers do unit testing after they complete coding a software component. When started writing code for a living, in my first project my project manager asked me to write down the unit test cases in a spread sheet and run these unit test cases against the component I completed developing and mark these unit test cases pass or fail.
Test-Driven Development is a software development process in which a developers should first write a unit test in code before writing component code, run it against the component that the developer is building , which is called System Under Test (SUT). The test will fail (symbolized by RED) then developers should develop the component, SUT, and re runs the tests, which would pass (GREEN) and finally developers should REFACTOR the new code if required to meet acceptable standards. TDD advocates repetition of the above RED, GREEN, REFACTOR in short cycles to develop components in software. Kent Beck is credited with developing and advocating TDD process to produce quality software. The RED, GREEN, REFACTOR cycle is shown in the below diagram.
TDD centers on the idea of Unit Testing and automated unit testing. Unit Test is defined as a test to verify functionality of a small unit of functionality in a component. Unit tests are done by developers. These are different from the tests done by testing team or integration testing or user acceptance testing. These tests have the lowest cost in software.
In Test-Driven Development a developer creates automated unit tests that define code requirements then immediately writes the code itself. The tests contain assertions that are either true or false. Passing the tests confirms correct behavior as developers evolve and refactor the code.
Test-Driven Development Life Cycle
The following sequence is based on the book Test-Driven Development by Example.
1. Add a test: In test-driven development, each new feature begins with writing a test. This test must inevitably fail because it is written before the feature has been implemented
2. Run all tests and see if the new one fails: This validates that the test harness is working correctly and that the new test does not mistakenly pass without requiring any new code.
3. Write some code: The next step is to write some code that will cause the test to pass.
4. Run the automated tests and see them succeed: If all test cases now pass, the programmer can be confident that the code meets all the tested requirements.
5. Refactor code: Now the code can be cleaned up as necessary. By re-running the test cases, the developer can be confident that code refactoring is not damaging any existing functionality.
6. Repeat: Starting with another new test, the cycle is then repeated to push forward the functionality.
Unit Testing Frameworks
Unit testing infrastructure has been developed by open source community / software vendors to facilitate automated unit testing. A number of code-driven unit testing frameworks have been developed for various programming languages. From a .NET perspective the popular frameworks are :
The below diagram depicts the unit testing framework and unit tests interaction. Unit testing framework provides a unit tests runner (exe), which executes all the unit tests and shared libraries that developer unit tests reference. These common libraries provide the necessary classes, methods, attributes to identify tests and assert statements.
A snapshot of NUnit unit’s test runner (exe) showing passed (GREEN) and failed (RED) test results.
Importance of Refactoring in TDD
The common theme of Test-Driven Development is Red, Green, and Refractor. Why is Refactoring important in TDD? Why should one bother about refactoring? Isn’t creating a failed test and passing it sufficient?
In software development, completing a component or a feature shouldn’t mean just writing code to get the required functionality working or just developing the feature. The life of software components span over multiple years. Developers should write code that is maintainable, readable, scalable and extensible.
These qualities are difficult to achieve while writing code for first time. The code should be revisited and improved to increase readability and maintainability of it. Imagine you have to write a mail or an article or writing something that a lot of people are going to read, how many times does one get it correct the first time they write it? One has to go over multiple iterations to get it right. Similarly in software development code has to be revisited multiple times to improve it. This is what is refactoring in software development.
When learning TDD, we should also learn the importance of test doubles. Let’s say that we are building a component (SUT) using Test-Driven Development, and the component is a Prescription Class which fills prescriptions. Now using TDD we are writing unit tests and we start a failing test to test FillRx method. Now we want to code Prescription class but the fill Rx depends on Patient object, prescriber object and Drug object. How do we pass in these dependency objects to SUT? How do we use TDD for developing the prescription class?
This is where Test Doubles come into picture; Test Doubles are like movie doubles, who perform stunts instead of real movie actors. These test doubles can be a simple integer that is passed a substitute for a value that the SUT is expecting or it can be a complex mock object that is configurable and closely resembles the data and behavior of the dependency object the SUT is expecting. These test doubles are passed as a reference to SUT in unit tests.
Different types of test doubles and what they do is listed below:
DUMMY: First type of Test Doubles is Dummy; it can be a simple integer or string that is passed as reference.
STUB: A minimal implementation of a class that likely implements some methods and interfaces. This doesn’t have state.
FAKE: A bit more sophisticated, contains a bit more complex implementation, has state.
SPY: A double that records the info about the interaction that it has with SUT so that the info is available to assert in the tests.
MOCK: Mock objects are the most sophisticated test doubles. Implementing this is not trivial. Many mock object libraries exist allowing us to simply configure to return results, interact with SUT. The 3 most common are TypeMock, RhinoMock.
Depending on configuration a mock can behave as a dummy, stub, fake or spy. It’s important to understand Test double and their usage while using Test-Driven Development process.
Benefits of TDD
The below diagram shows the benefits of Test-Driven Development and also shows how unit tests should be written using TDD.
When TDD is used as a software development process to build a component (SUT) the green layer around the SUT is how the unit tests should be written. If a new change introduced to System under Test (SUT) broke existing functionality or other features (depicted as the curve in the third picture), well written unit tests catch the broken behavior (depicted in Red) immediately. This way the developer can catch bugs that new code has introduced early increasing the quality of code and decreasing the cost of finding this bug at a later stage like testing or production itself.
Other Benefits of TDD are:
· Always accessible regression harness
· Higher quality of code with fewer defects
· Simplifies integration of components with other components
· Well written unit test might result in a living documentation, each test can be seen as a requirement
· Using TDD results in well crafted code, helps in design and results in SOLID design
· Low cost compared to other types of testing like Integration, system testing
· Test harness serves as a security blanket for the code when additional features and finds problems early in the development cycle
Test-Driven Development is a powerful software development process in building high quality software. It’s relatively difficult to follow and requires discipline and a different way of thinking, but once you learn TDD it has a number of advantages and improves the design of the component as well. The most important thing is the quality of unit tests, the unit tests written as part of TDD should be useful in testing the behavior of the SUT (which is what Behavior Driven Development is all about) than simply testing functions in the classes.