As one of the primary features of eXtreme Programming -- as well as many other Agile software development methodologies -- TDD can help developers create higher quality code on first iteration, as it helps developers avoid common errors of logic and design. Whether one subscribes to Agile or not, TDD is a great arrow to add to one's quiver of bug-slaying power. The tests serve as a form of documentation, making clear the proper usage of the code. A large test suite facilitates subsystem integration, as it provides immediate feedback when new changes break old code. Test results are explicit and discrete, giving a better overview of the development progress. In short, tested code is confident code. Any evangelical article on TDD will only cover these same points, with much more verbosity.
I will not cover the use of a specific unit testing framework at all. Strictly speaking, TDD can be performed without a unit testing framework. Also, bare-bones unit testing frameworks are fairly simple to develop. As with anything, it is more important to learn the concepts behind a technique, rather than the specifics of an implementation of a technique.
So, with that in mind, here is a quick review of the TDD lifecycle and some tips on how to write tests for code that doesn't yet exist, regardless of which programming language one uses, to which design methodology one adheres, or which testing framework one utilizes.
The TDD cycle:
- Take small implementation steps; large changes introduce large bugs,
- Design the implementation with testing in mind; determine what defines successful completion of the requirement and how to test for that completion,
- Write one test; resist the urge to code in large runs,
- Write enough code so that the tests compile without error; do not write an implementation -- just stubs,
- Run test tests -- new test should fail; if this is a tertiary pass through the TDD cycle, the changes may cause other, previously passing tests to fail,
- Write just enough code to pass all the tests; do not continue until all the tests pass, in this way one knows that one's changes do not break existing code,
- Refactor as necessary; minimize duplicate code, eliminate inefficiencies,
- Continue from step 2; consider if testing should be expanded, remember to test boundary cases of input (bad input, good input that is close to being bad, typical input, etc.),
- Return to step 1; continue implementing small pieces of the requirement.
Tips on writing good tests:
- How to move a mountain: blast it into pebbles. Small tests (as defined by the number of Assertions made by the test) have a very high “Testing Resolution”. A failed assertion immediately terminates the test, even if the test contains more assertions. Large tests with multiple assertions may hide assertion failures that occur after the first failed assertion. By splitting multi-assertion tests into single-assertion tests, all Assertions that can fail will fail, giving a higher resolution picture of the defect at hand. The test itself may be long on code to prepare for the Assertion, but it should only have one or two assertions.
- Test early, test often, test everything. Developers may feel like TDD requires more time than coding without testing. Initially, this is true. The developer not only takes the time to write the code, they also take the time to write the tests. The total amount of time spent on the code is far less, as higher quality code requires fewer bug fixes.
- The only good GUI is a thin GUI. How does one programmatically duplicate user interaction with reliability, precision, accuracy, and flexibility? Design GUI’s as only being a front end to well-tested, GUI-independent logic classes. Ensure the GUI elements are receiving the expected bindings and trust that the GUI elements of the Operating System work as advertised. This also aids in code reuse.
- In with the good data, out with the bad. How does one programmatically ensure that a map does not render 500 feet off kilter? In general, data validation must be completed via Regression Testing (comparison to known good results) or typically “by eye”.
This last point seems like a very large point against TDD. While it is true that there are large classes of problems to which TDD is not well suited, that does not make it a waste of time to learn TDD. There are no silver bullets in software development; software development is perhaps the single-most difficult human endeavour in history. With experience, the TDD novice will learn for which types of problems TDD works well, just as experience with Object Oriented Programming teaches when it is appropriate to utilize such features as inheritance, polymorphism, etc. TDD is one more tool to use when appropriate.
