With all the recent interest in the refactoring in C# in Whidbey, it's interesting to look at its natural complement, Test Driven Development (TDD). TDD is one of the best practices to come out of Extreme Programming and Agile Methodologies. When Refactoring combined with TDD are tools that can fundamentally change how developers work as well as changing the way they design. This post threads together posts on TDD and Refactoring from a ThoughtWorks and a Microsoft perspective that show the benefits and effects of TDD and Refactoring.
Some ThoughtWorks experience with TDD and Refactoring
Dan North, a ThoughtWorker, has an article in the Java Developers Journal called Test-Driven Development Is Not About Testing, his major point is that 'it's not about the tests; it's about seeing how little you actually need to do and how cleanly you can do it!' Jeremy Stell-Smith, also ThoughtWorker, has a reply post TddIsOnlyOnePieceOfThePuzzle where he makes the following statements about TDD:
- TDD is a tactical thing. It helps developers from adding unnecessary complexity and allows them to think about how the code will be used before other developers use it.
- It's important to refactor while using TDD to reduce duplication to keep the system clean.
- If a system has good automated testing and loose relationships between classes, then tests can give good feedback about the extent of damage that a change to the system might bring.
- Tests work best when they test small isolated areas of functionality that shouldn't break if anything else changes. For this reason, unit tests are easier to manage that end-to-end integration tests
There are also some points that Jeremy makes that seem a bit 'religious' such as 'changing your mind later is about as expensive as changing it later'. This just doesn't seem rational, but perhaps I'm just arguing for a shades of grey rather than this black and white style statement. I know that TDD makes it easier to make a change, or to measure the impact of a change, but it's still better to make changes as early as possible because it is more expensive to make changes later. However, I think that his assertion that 'tests must be as fine grained as possible so that most changes no matter how radical don't affect many tests' seems a fine goal, but is hard to achieve.
Early Microsoft experience with TDD
Chris Anderson points to Randy, a friend of his within Microsoft who's reflecting on his experience with TDD. Randy says that it takes a lot of discipline to stop writing code before a test case that uses it and that Visual Studio is a hindrance when working test first because the intellisense stops being helpful when the writing test code for methods that don't yet exist (Roy wrote a good post showing how IntelliJ handles this situation in a much cleverer way, hopefull Microsoft will pick this up and copy extend it). Randy also talks highlights how TDD can be difficult:
... it's a completely different ball game when you're changing the behavior of code as opposed to writing entirely new code. Say you've written a sizable body of code with inter-related classes and non-trivial implementations of whatever. Now suppose you want to make a fundamental change to one of the more central class's behaviors ... substantial build breaks where methods no longer exist, because the functionality they provided has been made unnecessary ... doing test first in this case is tricky. You can start by changing the test that exercises the fundamental class in the new way, and then changing the fundamental class to pass the test. Unfortunately, then you've got build breaks across your entire project, and fixing those involve work that should also be done in test-first fashion. The way I did this was to exclude all the other files from the project, get the fundamental tests working again, and slowly re-introduce the other functionality, tests first of course, fixing/updating as I go. Maybe it's the same amount of work in the end, but it feels more cumbersome
From my experience and the developers I've talked to at the Extreme Tuesday Club, if the code is difficult to test then it can highlight weaknesses in the design of the code (such as it being too tightly coupled). Put another way, designing code with testing in mind often leads to code that is better designed. The pain that Randy highlights when working with inter-related classes encourages a designs that are more loosely-related so that they can be more easily tested.
Mock Objects is an approach to making testing easier in large complex systems. As the authors of the definitive paper on Mock Objects say testing can improve code "by preserving encapsulation, reducing global dependencies, and clarifying the interactions between classes." More on that in a future post.