Test Driven Development
Test-driven development (TDD) is a special case of test-first programming that adds the element of continuous design.
Table of Contents
Test-first programming involves producing automated unit tests for production code, before you write that production code. Instead of writing tests afterward (or, more typically, not ever writing those tests), you always begin with a unit test. For every small chunk of functionality in production code, you first build and run a small (ideally very small), focused test that specifies and validates what the code will do. This test might not even compile, at first, because not all of the classes and methods it requires may exist. Nevertheless, it functions as a kind of executable specification. You then get it to compile with minimal production code, so that you can run it and watch it fail. (Sometimes you expect it to fail, and it passes, which is useful information.) You then produce exactly as much code as will enable that test to pass.
This technique feels odd, at first, to quite a few programmers who try it. It’s a bit like rock climbers inching up a rock wall, placing anchors in the wall as they go. Why go to all this trouble? Surely it slows you down considerably? The answer is that it only makes sense if you end up relying heavily and repeatedly on those unit tests later. Those who practice test-first regularly claim that those unit tests more than pay back the effort required to write them.
For test-first work, you will typically use one of the xUnit family of automated unit test frameworks (JUnit for Java, NUnit for C#, etc). These frameworks make it quite straightforward to create, run, organize, and manage large suites of unit tests. (In the Java world, at least, they are increasingly well integrated into the best IDEs.) This is good, because as you work test-first, you accumulate many, many unit tests.
Benefits of test-first work
Thorough sets of automated units tests serve as a kind of net for detecting bugs. They nail down, precisely and deterministically, the current behavior of the system. Good test-first teams find that they get substantially fewer defects throughout the system life cycle, and spend much less time debugging. Well-written unit tests also serve as excellent design documentation that is always, by definition, in synch with the production code. A somewhat unexpected benefit: many programmers report that “the little green bar” that shows that all tests are running clean becomes addictive. Once you are accustomed to these small, frequent little hits of positive feedback about your code’s health, it’s really hard to give that up. Finally, if your code’s behavior is nailed down with lots of good unit tests, it’s much safer for you to refactor the code. If a refactoring (or a performance tweak, or any other change) introduces a bug, your tests alert you quickly.
Test-driven development: taking it further
Test-driven development (TDD) is a special case of test-first programming that adds the element of continuous design. With TDD, the system design is not constrained by a paper design document. Instead you allow the process of writing tests and production code to steer the design as you go. Every few minutes, you refactor to simplify and clarify. If you can easily imagine a clearer, cleaner method, class, or entire object model, you refactor in that direction, protected the entire time by a solid suite of unit tests. The presumption behind TDD is that you cannot really tell what design will serve you best until you have your arms elbow-deep in the code. As you learn about what actually works and what does not, you are in the best possible position to apply those insights, while they are still fresh in your mind. And all of this activity is protected by your suites of automated unit tests.
You might begin with a fair amount of up front design, though it is more typical to start with fairly simple code design; some white-board UML sketches often suffice in the extreme programming world. But how much design you start with matters less, with TDD, than how much you allow that design to diverge from its starting point as you go. You might not make sweeping architectural changes, but you might refactor the object model to a large extent, if that seems like the wisest thing to do. Some shops have more political latitude to implement true TDD than others.
Test-first vs. debugging
It’s useful to compare the effort spent writing tests up front to time spent debugging. Debugging often involves looking through large amounts of code. Test-first work lets you concentrate on a bite-size chunk, in which fewer things can go wrong. It’s difficult for managers to predict how long debugging will actually take. And in one sense, so much debugging effort is wasted. Debugging involves time investment, scaffolding, and infrastructure (break points, temporary variable watching, print statements) that are all essentially disposable. Once you find and fix the bug, all of that analysis is essentially lost. And if not lost entirely to you, it is certainly lost to other programmers who maintain or extend that code. With test-first work, the tests are there for everybody to use, forever. If a bug reappears somehow, the same test that caught it once can catch it again. If a bug pops up because there is no matching test, you can write a test that captures it from then on. In this way, many test-first practitioners claim that it is the epitome of working smarter instead of harder.
Test-first technique and tools
It is not always trivial to write a unit test for every aspect of a system’s behavior. What about GUIs? What about EJBs and other creatures whose lives are managed by container-based frameworks? What about databases and persistence in general? How do you test that an exception gets properly thrown? How do you test for performance levels? How do you measure test coverage, test granularity, and test quality? These questions are being answered by the test-first community with an ever evolving set of tools and techniques. Tremendous ingenuity continues to pour into making it possible to cover every aspect of a system’s behavior with unit tests. For example, it often makes sense to test-drive a component of a system in isolation from its collaborators and external resources, using fakes and mock objects. Without those mocks or fakes, your unit tests might not be able to instantiate the object under test. Or in the case of external resources like network connections, databases, or GUIs, the use of the real thing in a test might slow it down enormously, while the use of a fake or mock version keeps everything running quickly in memory. And while some aspects of functionality may always require manual testing, the percentage for which that is indisputably true continues to shrink.