This style of programming gives you a continuity of development that is inaccessible otherwise. You find mistakes early enough that the DreadedDayOfDebugging becomes a distant memory. The effects of bugs in unrelated modules get detected when designing test setup.
You get to use debuggers as sharp, pointed, precise tools rather than as shovels for digging for mistakes among mounds of code. When a test fails, you set a breakpoint and trace. Don't try to second guess the computer. If the tests are fine enough, and the scaffolding small enough, debug runs end very quickly. You can think of this as ComputerAidedCodeInspection?.
It also means that you can refactor confidently, knowing that your tests will protect you from mistakes. In turn, refactoring constantly keeps design warts from becoming cancers.
When adding new functions, devise a new test. If the new feature suggests a change in the design, do it. Your earlier tests let you do it without an OhGod feeling lurking in the background. Then get the new test going.
You can turn this style around to understand programs. When trying to understand other people's code, write tests for it. Talk to them to find out what is their minimal scaffolding, and how to compare results.
I think of this programming style as a programmer's version of the Dijkstra/Hoare/Gries (EwDijkstra,CarHoare) development technique. The scaffolding is the "pre-condition", and the test is the "post-condition". Good tests look a lot like formulae: everything is reduced to equality. Refactoring feels like rearranging formulae.
I have used this style effectively in C++ programming on the Choices project, where slow machines and miserable environments discouraged rapid code turnaround, you had to compromise designs to achieve fast compilation, debugging was hard, and so on. Even so, it turned out that using well designed stubs of various type, incremental programming *was* possible. And the micro-testing meant that during kernel virtual memory development, when printf could fail, and debuggers cease to operate, and booting a machine (Encore Multimaxes) could take five minutes by the clock, and so on, this style ended up being very effective although it required a lot of care. (In the end, we ended up creating the ultimate stub: the whole operating system "ported" to the "Unix Abstract Machine", using mmap for VM, signals for interrupts, sigalarm for preemption, and files as disks).
The advocacy of this style in ExtremeProgramming finally cemented it for me. As usual, TomVanVleck pages mention this style as well; he calls it TestThenCode. Sometimes it seems that in programming (especially OSes), all roads lead back to Multics. -- AamodSane
Very nice, Aamod! -- RonJeffries Please turn this into an article!
I agree. Nice piece. For me, it no longer makes sense for me to do things in any other way. TestDrivenProgramming is like a virus (a good one!) that just spreads. When I think about how a function or method will work in terms of "outside the box" by looking at its interaction with the world (and the unit test), I just design better. And my code consistently works better earlier. Thanks for writing this.
I'm sure KentBeck won't agree with you on that point: TestDrivenDevelopment.
There are a growing number of <noun>-driven's driving these days. Since most significant software is developed for others, shouldn't development be CustomerValueDrivenDevelopment?
Yes. TestDrivenDevelopment is a way of achieving CustomerValueDrivenDevelopment.
Since tests are also code, shouldn't we be writing tests to test the tests? :) -- HemantSahgal?
The code we write the tests for is the test for the tests.
On a more serious note – how does one ensure the correctness and completeness of tests? Test driven development and refactoring will work only when we have all the possible tests (and how will that be verified?). Of course some tests are better than no tests… -- HemantSahgal?
TestTheTest and BugsInTheTests explore the issues. TestsCantProveTheAbsenceOfBugs, it's better to view TestsAsScaffolding. TestFirst produces good design and lets you RefactorMercilessly. --JoeWeaver
I acknowledge that tests can only guarantee the success of the tests. This success may not be the success I want. However, the existence of tests gives me far more control and confidence in my code than avoidance of tests gives me. So, I test. If there's a better alternative, I'd love to hear it, but this is the best solution so far. -- BrentNewhall
In test driven design, the tests document exactly what the code does. No code features are implemented except as required by the tests, so by definition, we have 100% test coverage for the code. Any operations outside the scope of tests are undefined; any result is acceptable. When using test first design, the definitions are reversed. When all of the tests pass, the code is at fault if it does more than required by the tests. In contrast, in a code before testing approach, the tests are at fault if they do not cover everything the code does. The difference is in the first case, the tests are the definition for the code, and in the latter case, the code is the definition for the tests. This is also more than a semantic difference, it is a true process difference. --WayneMack
See: CodeUnitTestFirst, TestFirstDesign, TestDrivenDevelopment, TestDrivenAnalysisAndDesign
This page mirrored in ExtremeProgrammingRoadmap as of April 29, 2006