Most test-driven development (TDD) advocates claim that TDD generates well designed code. I think this thesis is in essence true, but conceals some conditions and intermediate steps. The direct outcome of test-driven development is not a design, but testable code and tests that enable refactoring.
Testable objects usually have their dependencies injected and in general clean interfaces, which increases the likelihood of loose coupling with other objects. These are characteristics of well designed objects. So, objects that are created with testability in mind (as they are with TDD, since you just wrote a test for the object at hand) are more likely to be well designed.
Being able to refactor your code allows you to change your mind about your design while you program and therefore to improve it as you go along. It is infinitely harder to come up with the best possible design up front than to start with something simple but imperfect and improve in small steps. This is the most powerful weapon of TDD. Your test suite provides a safety net and gives you the confidence to make the necessary changes to improve the design of your code.
Practicing TDD does not guarantee the above though, because
- Doing TDD right requires skill and
- TDD only increases the odds of good design, but certainly does not guarantee it. It still requires skill to make the right design decisions.
How do you do TDD right?
- Test code quality: If your tests are of poor quality, you can’t expect them to improve anything and they will become a burden rather than being of use. Your tests need to be understandable, after all they are supposed to be your executable specification.
- Testing on the right level: Your tests should test the behaviour of your objects, not their implementation. As such they need to test the communication of an object with other objects, i.e. the messages passed between them (aka method calls). In Java, for instance, you should write tests that invoke the method under test and then verify the return value and that the method called the expected methods of other (mock) objects. Don’t test how the method computed its result (= implementation detail).
What is TDD for?
Because of all this TDD (quite rightly) is considered to be a design technique rather than a programming technique, but one that happens to let you do your design, while you program. I do however believe that TDD is a design technique for the more fine-granular parts of your system (like the shape of interfaces) and that it does not make a high level design and architecture obsolete.