Did that title get your attention? Good.
Like the other #no<x> assertions – NoEstimates, NoBugs – I'm really not saying that you shouldn't do TDD. Well, maybe I am…
I was an early TDD advocate, as I really liked the way it helped me organize my thoughts, and I respected some of the other people who were early advocates – people like Ron Jeffries.
But looking back on the 15 years since I started with TDD, I have to say that it really did not live up to my expectations.
What I've seen is a consistent pattern of TDD working in a laboratory setting – developers are quick to pick up the workflow and can create working code and tests during classes/exercises/katas – and then failing in the real world.
My hypothesis for this is very simple. When you look at the TDD evangelists, all of them share something: they are all very good – probably even great – at design and refactoring. They see issues in existing code and they know how to transform the code so it doesn't have those issues, and specifically, they know how to separate concerns and reduce coupling.
If you have those skills, TDD works great; you see what the issues are as the arise and fix them incrementally, and your simple tests prove that your design has low coupling. And, you have the tests to lean on in the future.
Let's take the TDD workflow and remove the third step – the refactoring step. What would we expect to happen?
Well, we would expect to end up with classes that have multiple concerns in them – because we didn't split them apart – and they would be inconvenient to test. We would need to write a lot of test code, and would need a lot of help to write the code, either creating many hand-written mocks or using mock libraries.
Which I submit is precisely the result that most developers get using TDD; the resulting code looks exactly like what we would expect if developers are skipping the third step. Which isn't really very surprising, given that the tests in most non-TDD code look the same way, and we know that most developers do not have great design/refactoring skills.
At this point we should ask ourselves, "are these developers getting a net positive benefit from using TDD?" Let's list the pros/cons:
We end up with tests that verify the behavior of the code and help prevent regressions
It takes us longer to write code using TDD
The tests get in the way. Because my design does not have low coupling, I end up with tests that also do not have low coupling. This means that if I change the behavior of how class <x> works, I often have to fix tests for other classes.
Because I don't have low coupling, I need to use mocks or other tests doubles often. Tests are good to the extent that the tests use the code in precisely the same way the real system uses the code. As soon as I introduce mocks, I now have a test that only works as long as that mock faithfully matches the behavior of the real system. If I have lots of mocks – and since I don't have low coupling, I need lots of mocks – then I'm going to have cases where the behavior does not match. This will either show up as a broken test, or a missed regression.
Design on the fly is a learned skill. If you don't have the refactoring skills to drive it, it is possible that the design you reach through TDD is going to be worse than if you spent 15 minutes doing up-front design.
I think that's a good summary of why many people have said, "TDD doesn't work"; if you don't have a design with low-coupling, you will end up with a bunch of tests that are expensive to maintain and may not guard against regression very well.
And to be fair, the level of design and refactoring skills required is relative to the state of the codebase you're working in. There are a lot of legacy codebases with truly monumental amounts of coupling in them and figuring out how to do anything safely is very difficult and complex.
Instead of spending time teaching people TDD, we should instead be spending time teaching them more about design and especially more about refactoring, because that is the important core skill. The ability to write and refactor code to a state with low coupling, well-separated concerns, and great names will be applicable in all the codebases that we work in.
That is not to say that I don't dust off Ncrunch now and then to either create a new set of functionality with great tests or to create tests around a concern as I do a refactoring, because I still do that often. I just don't think it's the right place to start from a educational or evangelical perspective.