views:

164

answers:

5

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?

+2  A: 

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.

BobTurbo
Not sure I'm convinced that TDD isn't overkill for this purpose, but this seems to be the most sensible motivation for TDD.
Rei Miyasaka
A: 

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.

John Saunders
+2  A: 

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
http://stephenwalther.com/blog/archive/2009/04/11/tdd-tests-are-not-unit-tests.aspx

Peter Gfader
+3  A: 

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.

Yishai
Hmm... forcing you to write "just enough" code is actually an effect of the forced composability too. Understanding things like referential transparency and side effects will force your code to do "just enough" just as well.Refractoring is what I refer to in my second point, that it makes code more composable. Composable software is by definition easy to refractor.In most cases, with certain rules in mind, I can derive OOP patterns from functional style that yield modular code that tends to look and feel a lot like TDD code.
Rei Miyasaka
@Rei, if your argument is that it is theoretically possible to get TDD like code without TDD, no one argues with you (even hard core advocates of TDD). However, that doesn't make it likely or as easy.
Yishai
Regarding refactoring, it doesn't matter how composable you make your software, if you want to refactor the internal structure of your code, you will be afraid to do so without a safetey net. Unless you are unusually good or unusually foolish.
Yishai
@Yishai: Indeed, it's possible to write great OO code without TDD - but most don't, and TDD is a great way of teaching what good OO code is.
kyoryu
+1  A: 

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.

P.K