I have seen TDD code end up with terrible design that is hard to change.
The reason, as far as I can tell, seems to be that the TDD cycle isn’t completed. This combined with having a poor starting point for the initial test.
The general idea is that you write a failing test, make that test pass, then refactor your code.
In Kent Beck’s book, he describes this last step as ‘eliminate duplication’.
In the code I have seen that goes awry, the refactoring step hasn’t gone far enough. Sure, the code is ‘tidied up a bit’. A couple of methods extracted. Names improved. But often, whole concepts of interfaces and classes are just missing. There’s plenty of code around that does nearly the same thing, but without clear abstractions being pulled out.
Then the next cycle starts.
This often leaves the code with locked-in implementation details, as tests suddenly become more prescriptive in how methods are coded than they should be. And, ironically, these tests make the code more difficult to refactor. They are supposed to make it safer to do that.
The code can then diverge. Two closely related ideas get developed in different ways. Think of a case where an if statement gets added in one class, that used to be similar to another one. Now, it’s not as similar. Refactoring both classes *now* will be hard, because it is harder to see the similarities.
As the test base grows, and the software grows out, it is common to see a small change affecting a lot of test setups and expectations. I think the TDD idea is that this never happens. But it does. And even finding out the reason why does not seem to make it any easier to avoid.
The other major issue is in step one. “Let’s write a test”.
Where? Testing what? With a view to driving out what design?
I’ve been involved with TDD where this very notion of thinking about design upfront was discouraged. Again, in theory, it will all emerge serendipitously in the refactor step. But it doesn’t always. And the further away from ‘perfect design’ your first test was, the less likely it is to get there.
Duplication and other design failings get harder to spot in large Agile teams.
I may write ‘ReaderOfFiles’ class. My colleague might not have known this exists, and TDDs ‘FileReader’ class. Neither of us know about ithe other's existence. Nor does the third programmer, who in two weeks TDDs ‘FileRepository’, which of course does almost exactly the same thing.
With legacy code, the situation is worse. Where do you start with your test, given you are not free to decide? You have to work with the software that is already written. Hook into the frameworks that are already there. I find this a very difficult step.
Again, a ‘spike’ solution is proposed. That is, code-and-fix a solution, make notes on which classes were involved, revert it all, then try to re-build a better solution using TDD. Limited success for me. And my personality cannot stand doing the same work twice in succession. So for me at least, human factors play into this.
This is doubly unfortunate, as after about 1,000 lines of code, line 1 is effectively ‘legacy’. Who wrote it? Why is it there, again? Can/should it be replaced now? All questions of legacy software.
TDD does have many advantages.
It is a way you can engage juniors in a design process, as any and all design is emergent. You don’t need 20 years of UML and patterns for that to happen.
It gives great test coverage.
It does encourage thinking about the high level splits of your software. It encourages decoupling.
It does document certain edge cases in the software.
But like most software Silver Bullets, I think it gets viewed through rose tinted glasses sometimes. It’s good - but will it automagically produce perfectly designed software, that’s easy to maintain? Not guaranteed.
It’s important to remember that TDD adds only one inventive step: the assert. That’s it. It makes no separate contribution to design, leaving that simply as the advice to ‘refactor your software’. And I’ve found even that to be too weak to guarantee clean designs, in practice.
So TDD good? Yes. Problem free? No.