views:

1010

answers:

9

I have mixed feelings about TDD. While I believe in testing I have a issues with the idea of the test driving my development effort.

When you code to satisfy some tests written for an interface for requirements you have right now, you might shift your focus from building maintainable code, from clean design and from sound architecture.

I have a problem with driven not with testing. Any thoughts?

+1  A: 

It's always a balance:
- too much TDD and you end up with code that works, but is a pain to work on.
- too much 'maintable code, clean design, and sound architecture' and you end up with Architecture Astronauts that have talked themselves into coding paralysis

Moderation in all things.

pjz
+30  A: 

No.

If done right, Test Driven Development IS your design tool.

I hope you forgive me for linking to my own blog entry, wherein I discuss the pitfalls of Test Driven Development that went wrong simply because developers treated their tests as, merely, tests.

In a previous project, devs used a highly damaging singleton pattern that enforced dependencies throughout the project, which just broke the whole thing when requirements were changed:

TDD was treated as a task, when it should have been treated as an an approach. [...]

There was a failure to recognize that TDD is not about tests, it’s about design. The rampant case of singleton abuse in the unit tests made this obvious: instead of the test writers thinking “WTF are these singleton = value; statements doing in my tests?”, the test writers just propagated the singleton into the tests. 330 times.

The unfortunate consequence is that the build server-enforced testing was made to pass, whatever it took.

Test Driven Development, done right, should make developers highly aware of design pitfalls like tight coupling, violations of DRY (don't repeat yourself), violations of SRP (Single Responsibility Principle), etc.

If you write passing code for your tests for the sake of passing your tests, you have already failed: you should treat hard to write tests as signposts that make you ask: why is this done this way? Why can't I test this code without depending on some other code? Why can't I reuse this code? Why is this code breaking when used by itself?

Besides if your design is truly clean, and your code truly maintainable why is it not trivial to write a test for it?

Jon Limjap
What about depending on frameworks which were not designed to be test friendly ? (the majority of them I think) not to mention legacy code. I do like TDD and practice it myself but I often find the need for a quick diagram on the corner of the table to make sure I got it all right
Jean
As for legacy code, that's a totally different story altogether. :D
Jon Limjap
There is an implicit need to have some basic design... you need that to be able to write your first tests. The concern is not against Design Up Front per se, it's about BIG design up front before the best design for specific features/tasks are arrived at.
Jon Limjap
Jean, you still have a design and you still draw diagrams and scribble boxes and lines it's just that you let the tests act as the first user for the code that you're designing and, by having that first user very early on you get feedback about what's wrong and what's right VERY quickly.
Len Holgate
+1  A: 

There are three steps to complete software:

  1. Make it work
  2. Make it right
  3. Make it fast

Tests get you #1. Your code is not done just because the tests have passed. Preferably you have some concept of project structure (Utilities, commonly accessed objects, layers, framework) before you start writing your tests/code. After you've written your code to make the tests pass, you need to re-evaluate it to see which parts can be refactored out to the different aspects of your application. Yuo can do this confidently, because you know that as long as your tests are still passing, you code is still functional (or at least meeting the requirements).

At the start of a project, give thought to the structure. As the project goes on continue to evaluate and re-evaluate your code to keep the design in place or change the design if it stops making sense. All of these items must be taken into account when you estimate, or you will end up with spagetti code, TDD or not.

Tom Carr
+1  A: 

I completely agree with pjz. There is no one right way to design software. If you take TDD to an extreme, without any forethought except the next unit test, you may make things harder on yourself. Ditto for the person who sets out on a grand software project by spending months on diagrams and documentation, but no code.

Moderate. If feel the urge to draw up a quick diagram that helps you visualize the structure of your code, go for it. If you need two pages, it might be time to start writing some code. And if you want to do that before you write your tests, so what. The goal is working, quality software, not absolute conformity to any particular software development doctrine. Do what works for you and your team. Find areas where improvements can be made. Iterate.

+1  A: 

I'm relatively new to TDD and unit testing, but in the two side projects I've used it on, I've found it to be a design aide rather than alternative to design. The abilty to test and verify components / sub-components independently has made it easier for me to make rapid changes and try out new design ideas.

The difference I've experienced with TDD is reliability. The process of working out component interfacing on smaller levels of component at the begining of the design process, rather than later, is that I've got components I can trust will work earlier, so I can stop worrying about the little pieces and instead get to work on the tough problems.

And when I inevitably need to come back and maintain the little pieces, I can spend less time doing so, so I can get back to the work I want to be doing.

Aaron
+1  A: 

For the most part I agree that TDD does provide a sort of design tool. The most important part of that to me is the way that it builds in the ability to make more changes (you know, when you have that flash of insight moment where you can add functionality by deleting code) with greatly reduced risk.

That said, some of the more algorithmic work I've contracted on lately has suffered a bit under TDD without a careful balance of design thought. The statement above about safer refactoring was still a great benefit, but for some algorithms TDD is (although still useful) not sufficient to get you to an ideal solution. Take sorting as a simple example. TDD could easily lead you to a suboptimal (N^2) algorithm (and scads of passing tests that allow you to refactor to a quick sort) like a bubble sort. TDD is a tool, a very good tool, but like many things needs to be used appropriately for the context of the problem being solved.

+3  A: 

I completely agree with you on that subject. In practice I think TDD often has some very negative effects on the code base (crappy design, procedural code, no encapsulation, production code littered with test code, interfaces everywhere, hard to refactor production code because everything is tightly coupled to many tests etc.).

Jim Coplien has given talks on exactly this topic for a while now:

Recent studies (Siniaalto and Abrahamsson) of TDD show that it may have no benefits over traditional test-last development and that in some cases has deteriorated the code and that it has other alarming (their word) effects. The one that worries me the most is that it deteriorates the architecture. --Jim's blog

There is also a discussion over on InfoQ between Robert C. Martin and James Coplien where they touch on this subject.

Bjorn Reppen
If the code degenerates into procedural, without encapsulation, then TDD wasn't the issue. It's OOP. See my answer
Jon Limjap
+4  A: 

There's always a risk of overdoing either the TDD design or the upfront design. So the answer is that it depends. I prefer starting with a user story/acceptance test which is the base of the requirement that my tests will aid in producing. Only after I've established that, I start writing detailed unit tests TDD-style. If the only design and thinking you do is through TDD, then you risk too much of a bottom up approach, which might give you units and classes that are excellent in isolation, but when you try to integrate them into the user story fulfilling task you might be surprised by having done it all wrong. For more inspiration on this, look att BDD.

A great "debate" about this has been recorded between Robert C. Martin and James Coplien, where the former is a TDD advocate and the latter has stated that it ruins the design of a system. This is what Robert said about TDD and design:

"There has been a feeling in the Agile community since about '99 that architecture is irrelevant, we don't need to do architecture, all we need to do is write a lots of tests and do lots of stories and do quick iterations and the code will assemble itself magically, and this has always been horse shit. I even think most of the original Agile proponents would agree that was a silliness."

James Coplien states that merely driving your design from TDD has a great risk:

"One of the things we see a lot, in a lot of projects, is that projects go south on about their 3rd sprint and they crash and burn because they cannot go any further, because they have cornered themselves architecturally. And you can't refactor your way out of this because the refactoring has to be across class categories, across class hierarchies, and you no longer can have any assurances about having the same functionality."

Also he gives a great example of how a bank account probably would look if you test drove it as compared to using your upfront knowledge to drive the architecture:

"I remember when I was talking with Kent once, about in the early days when he was proposing TDD, and this was in the sense of YAGNI and doing the simplest thing that could possibly work, and he says: 'Ok. Let's make a bank account, a savings account.' What's a savings account? It's a number and you can add to the number and you can subtract from the number. So what a saving account is, is a calculator. Let's make a calculator, and we can show that you can add to the balance and subtract from the balance. That's the simplest thing that could possibly work, everything else is an evolution of that.

If you do a real banking system, a savings account is not even an object and you are not going to refactor your way to the right architecture from that one. What a savings account is, is a process that does an iteration over an audit trail of database transactions, of deposits and interest gatherings and other shifts of the money. It's not like the savings account is some money sitting on the shelf on a bank somewhere, even though that is the user perspective, and you've just got to know that there are these relatively intricate structures in the foundations of a banking system to support the tax people and the actuaries and all these other folks, that you can't get to in an incremental way. Well, you can, because of course the banking industry has come to this after 40 years. You want to give yourself 40 years? It's not agile."

The interesting thing here is that both the TDD proponent and the TDD antagonist are saying that you need design up front.

If you have the time, watch the video. It's a great discussion between two highly influential experts, and it's only 22 minutes long.

Microserf
Thanks Microserf :) I watched the video. My feeling is the same. If you put the main focus on testing the code and make tests mandatory they will somehow start to substitute for good design and architecture.
Daniel
Which is the wrong way to approach TDD -- focusing on testing per se is badly practiced test-driven development.
Jon Limjap
+2  A: 

My way to think about it is, write what you want your code to look like first. Once you have a sample of your target code (that right now does nothing) see if you can place a test scaffolding onto it. If you can't do that, figure out why you can't. Most of the time it's because you made a poor design decision (99%), however if that's not the case (1%) try the following:

  • determine what the crazy requirements are that you need to abide to that wont let you test your code. One you understand the issues redesign your API.
  • if someone else decided this requirements discuss about it him/her. They probably had a good reason for the requirement and once you know their reason you'll be able to perfect your design and make it testable. If not now you can both rework the requirement and you'll both be the better for it.

After you have your target code and the test scaffolding. Implement the code. Now you even have the advantage of knowing how well your progressing as you pass your own test (Its a great motivator!)

The only case where testing may be superfluous, from personal experience, is when you are making an early prototype because at that point you still don't understand the problem well enough to design or test your code accurately.

Robert Gould