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?
TDD helps you to develop the logic in your code By starting tests with the simplest functionality first, you can use them to guide your logic as you build up functionality. This helps you to break a problem down into smaller, more manageable pieces, thus aiding the problem solving process.
It is a continuous process that includes refactoring, unit testing, and programming. Prior to writing any actual code, Test-driven development (TDD) emphasizes the creation of unit test cases. It requires developers to build a test first, then just enough production code to satisfy it and the ensuing reworking.
In terms of design, one major benifit of TDD you are not seeing is that it drives design to be just enough. You know what they say the engineer sees the glass as twice as large as it should be. Overdesign in software can be a big problem. I find that 90+% of the time TDD forced out the right ballance of abstraction to support later extension of the code. TDD isn't magic, it takes the programmer behind it to do that as well, but it is an important part of the toolkit.
I think that there is too much TDD in isolation in your list. What about refactoring? I think one of the prime benifits of the test is that it locks down behavior, so that when you refactor you can be confident that you haven't changed anything, which in turn can make you confident about refactoring. And there is nothing like a design which is born from experience rather than a whiteboard (although high-level whiteboard design is still very important).
Also, you write: "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?" Code that is written without unit testing in mind is often untestable, especially if it interacts with outside services such as a database, transaction manager, GUI toolkit or web service. Adding them later is just not going to happen.
I think Bob Martin said it best, TDD is to programming what Double Entry is to Accounting. It prevents a certain class of mistakes. It doesn't prevent all problems, but does make sure that if your program intended to add two plus two, it didn't subtract them instead. Basic behavior is important, and when it goes wrong, you can spend a lot of time getting to know your debugger.
I believe the benefit of TDD is that you actually write the tests as they are more interesting when they are a goal you have to achieve, (create code to pass the tests), rather than a chore you have to do afterwards.
Also, it puts you in the mind of the user. You have to think "so what does the user need my method to do" or whatever, rather than, "I hope my method has achieved what it was supposed to do". In this way, it may also help to reduce bugs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With