Test-Driven Development By Example
The Perl Journal April 2003
By Tim Kientzle
Tim is a freelance software developer and consultant. He can be reached at firstname.lastname@example.org.
Test-Driven Development By Example
220 pp., $29.99
A few weeks ago, I started developing a new library. The first step, of course, was to write a short test program that used the library. This gave me a chance to think through the library interface carefully and see if it would really be useful in the situations I envisioned. The next step was to stub out the library functions so that the test program would actually compile and run. Only then was I ready to start implementing the library, testing each small addition as I went.
This informal approach grew out of my personal experience, heavily influenced by the writings of Fred Brooks, Donald Knuth, and Leo Brodie. However, Test-Driven Development (TDD), as it is now known, has been recognized as a formal software development technique and carefully refined and documented as a part of the Extreme Programming (XP) methodology.
Kent Beck's book Test-Driven Development By Example takes a careful look at the ideas and rationale behind TDD in its own right. As such, it is one of the first thorough introductions to TDD that can be used by developers working alone or working on teams that have not adopted XP.
The book starts with two carefully presented examples. Through these examples, Beck introduces TDD as a minute-by-minute approach to software development, a way to organize your work so that you are continually realizing small, achievable goals. The approach can be succinctly summarized as a three-step cycle:
1. Red: Write the minimal test code to break your implementation.
2. Green: Write the minimal implementation to pass your tests.
3. Refactor: Clean up any code duplication introduced by the above.
You should take the word "minimal" very seriously. Here's a brief summary of one development sequence from the book:
- Write a test that invokes a multiplication function with arguments 5 and 2, and checks that the result is 10.
- Test fails (program doesn't compile) because the function is not implemented.
- Implement multiplication function to return constant 10.
- Test succeeds.
The lesson here is subtle. On the one hand, your test code can be thought of as a list of flaws in your implementation. Conversely, your implementation provides a way to explore flaws in your test code. If an obviously incorrect implementation satisfies your tests, then your test code itself must be incomplete. In this case, the next step is to beef up your test code until the only reasonable implementation that will pass the tests is the obviously correct multiplication function.
Of course, tiny development steps don't always create clean code. That's why TDD requires you to continually examine your code for ways to improve the structure and eliminate duplication. This is another reason to minimize your implementation at each stepextraneous code makes it more difficult to clarify and improve the structure.
At its best, this cycle generates a positive feedback loop: Because each small change to the implementation is preceded by an addition to your test code, you can have nearly perfect test coverage. Because you have nearly perfect test coverage, you can have a high confidence that code reorganizations won't break any existing functionality. Because you are constantly striving to keep your code clean and well organized, you are able to make small changes that incrementally add new capabilities.
The example above is taken from the first section of Beck's book, which develops a currency-manipulating class. The slow pace here allows the author to make his message clear"small steps, test first, clean as you go"and gives readers some space to think about how this approach might apply to their own work. By the end of this section, I saw many ways to improve my own informal use of TDD.
The middle section works through a more ambitious example: a full testing framework for Python, itself developed using TDD. I found this portion of the book considerably more confusing than it should have been. This is partly because I don't know Python very well and the author's promised "commentary on Python, for those...who haven't seen it before" never really materialized. More seriously, the author starts writing code and verifying small details without ever discussing the overall architecture. He also refers to a number of concepts that are not explained until much later in the book. Although there is some useful material here, I suggest skipping this entire section on a first reading.
Fortunately, the final part of the book is worth the wait. Here, the author uses a series of patterns to more deeply explore the mechanisms and philosophy of TDD. I found the refactoring patterns especially interesting, partly because they are really development patterns and not design patterns. Rather than describing a common code structure, they explore common approaches for developing code, such as breaking a single function in two or combining redundant functions into one.
Although this is a good book overall, I did find a few annoying problems: You'll need a passing familiarity with Java, Python, and SmallTalk to follow all of the examples. The Python code was not always correctly indented. The "traffic light" metaphor is heavily used and never explained. Similarly, the xUnit test architecture is never really clearly explained.
Extreme Programming is not a monolithic creation. Rather, it is a collection of techniques, not all of which are appropriate for every development team. By presenting TDD independently of XP, Beck is opening these ideas to a much wider audience. Ironically, this book may be responsible for many more people easing their way into XP. Small steps often work best.