I just bought The Art of Unit Testing from Amazon. I'm pretty serious about understanding TDD, so rest assured that this is a genuine question.
But I feel like I'm constantly on the verge of finding justification to give up on it.
I'm going to play devil's advocate here and try to shoot down the purported benefits of TDD in hopes that someone can prove me wrong and help me be more confident in its virtues. I think I'm missing something, but I can't figure out what.
1. TDD to reduce bugs
This often-cited blog post says that unit tests are design tools and not for catching bugs:
In my experience, unit tests are not an effective way to find bugs or detect regressions.
...
TDD is a robust way of designing software components (“units”) interactively so that their behaviour is specified through unit tests. That’s all!
Makes sense. The edge cases are still always going to be there, and you're only going to find the superficial bugs -- which are the ones that you'll find as soon as you run your app anyway. You still need to do proper integration testing after you're done building a good chunk of your software.
Fair enough, reducing bugs isn't the only thing TDD is supposed to help with.
2. TDD as a design paradigm
This is probably the big one. TDD is a design paradigm that helps you (or forces you) to make your code more composable.
But composability is a multiply realizable quality; functional programming style, for instance, makes code quite composable as well. Of course, it's difficult to write a large-scale application entirely in functional style, but there are certain compromise patterns that you can follow to maintain composability.
If you start with a highly modular functional design, and then carefully add state and IO to your code as necessary, you'll end up with the same patterns that TDD encourages.
For instance, for executing business logic on a database, the IO code could be isolated in a function that does the "monadic" tasks of accessing the database and passing it in as an argument to the function responsible for the business logic. That would be the functional way to do it.
Of course, this is a little clunky, so instead, we could throw a subset of the database IO code into a class and give that to an object containing the relevant business logic. It's the exact same thing, an adaptation of the functional way of doing things, and it's referred to as the repository pattern.
I know this is probably going to earn me a pretty bad flogging, but often times, I can't help but feel like TDD just makes up for some of the bad habits that OOP can encourage -- ones that can be avoided with a little bit of inspiration from functional style.
3. TDD as documentation
TDD is said to serve as documentation, but it only serves as documentation for peers; the consumer still requires text documentation.
Of course, a TDD method could serve as the basis for sample code, but tests generally contain some degree of mocks that shouldn't be in the sample code, and are usually pretty contrived so that they can be evaluated for equality against the expected result.
A good unit test will describe in its method signature the exact behavior that's being verified, and the test will verify no more and no less than that behavior.
So, I'd say, your time might be better spent polishing your documentation. Heck, why not do just the documentation first thoroughly, and call it Documentation-Driven Design?
4. TDD for regression testing
It's mentioned in that post above that TDD isn't too useful for detecting regressions. That's, of course, because the non-obvious edge cases are the ones that always mess up when you change some code.
What might also be to note on that topic is that chances are good that most of your code is going to remain the same for a pretty long time. So, wouldn't it make more sense to write unit tests on an as-needed basis, whenever code is changed, keeping the old code and comparing its results to the new function's?