Trying to gain confidence in the benefits of TDD
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 l开发者_运维问答arge-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?
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.
TDD is not a methodology, it is a mindset.
TDD to reduce bugs : As your code base starts growing, it is very important that you run all tests on each checkin to the source control. The benefit of this becomes evident when you have a new member in the team.
TDD as a design paradigm: Tests are first users of the code. Initially, it is very difficult to drive your design using tests. But, once you are comfortable with it, you will realize that the test code actually helps you decide on your design. This comes naturally with some TDD experience. For example, with TDD you would like to wrap access to your service in an interface. This allows you to mock. But, most important thing here is that it is the correct way to design.
TDD as documentation The documentation is for code-documentation meant to be used by developers of your code. Developers find it easy to read well-written unit tests, rather than pages and pages of documentation.
In Javascript, or any language that has to run in multiple quirky environments like browsers, unit tests are a great way of telling where Internet Effing Explorer messed up what to fix.
At least it's better than hammering F5 and using console.log()
or even alerts.
But then again, I think this should be used mostly for middleware or complex RIA - as it is almost impossible to TDD user interfaces with little business logic.
If you write the next jQuery, unit tests will be your friends! :)
Oh and let's not forget the great tools for js like JSTestDriver!
I just wanted to highlight:
TDD Tests are not Unit Tests
"The purpose of a unit test is to test a unit of code in isolation."
"A TDD test, unlike a unit test, might test more than a single unit of code at a time." TDD Tests are more interaction tests....
from Stephen Walter's very good blog post TDD Tests are not Unit Tests
TDD also gives you code that is thoroughly covered by automated unit tests. This is simply because no code is written until it's neccessary to make a failing unit test pass.
精彩评论