The idea behind test-driven design (TDD) is to incorporate testing into the process of constructing your system, rather than waiting for developers to check in their code and to build a system that someone else tests. TDD has some advantages worth considering:
- Defects are identified sooner in the development process, which makes delivery of a reliable system cheaper and quicker.
- Developers have more scope to improve the quality of code by refactoring, since they can run unit tests as they are making changes to verify whether the system still behaves as expected.
- Unit tests serve as built-in documentation on how to call and make use of interfaces.
- This built-in documentation is guaranteed not to go out of date. (You've never had the joy of working with out-of-date documentation, have you? You haven't lived until you've debugged some code whose documentation does not match. Or perhaps I should say, you haven't worked in software development unless you've debugged, etc.)
- A team can run unit tests in the final step of the build process, in order to verify that the new build has no obvious defects.
The test-driven design process goes like this:
- Get the requirement from the customer.
- Design a test suite that will verify that the requirement is satisfied.
- Run the test suite and make sure it fails.
- Write code and run the test suite until it succeeds.
When the test suite succeeds and there are no obvious ways to simplify the code, the developer performs his customary victory dance and checks in the code.
I have incorporated TDD into the development process for some web services that have multiple customers. If we did not have a more or less complete suite of unit tests, any modification would be hard to manage. Any codepath that any customer relies on could have a regression when any code is changed, right? So in the absence of TDD, any modification would require that several testers (Pat, Tammy, and Charles at our office) spend a fair amount of their precious time running regression tests. Consequently, I have created suites of unit tests to verify that no regressions have occurred, and have even given them names ("virtual Pat," "virtual Tammy," and "virtual Charles") . Virtual Pat and her virtual colleagues are working hard so that the real Pat and her colleagues can head home at 5pm.
Recently I received a requirement to modify a service method so that it would return extra data. After I revised the unit tests, I almost skipped over step #3 (make sure the test fails). After all, I was in a hurry, and why bother? Aren't my coding skills good enough to write a simple test?
Evidently not.
I ran the test suite and it did not fail. I rubbed my eyes in disbelief and ran the suite again; of course, it succeeded again. A quick glance at the tests showed that the test suite contained the extra data expected from the method, but I had failed to write the assertions that would compare the expected data with the actual data returned from the method. So if the actual data returned from the method was not correct, the test (and the developer) would never know it. Doh! <Sound of hand smacking forehead />
So I added the assertions, ran the test suite (Hurrah! It failed!), wrote the new code, ran the test suite again, and it succeeded. I checked in my code, and it's working as expected.
The moral of this tale is simple: in test-driven design, you have to fail before you can succeed.