Adventures in Pure Test-Driven Development

In a previous post some time back, I had expressed some skepticism about TDD as a design practice. I talked about test-driven development and its relationship with prototyping and the “make one to throw away” concept.

Since I’m not one ever to believe that I’ve arrived at the optimal solution, I’m doing another round of pure TDD to see if I can ‘refactor’ my process a little and find improvements in efficiency and/or code quality. As I’m doing this, I’m finding that I’m a little more proficient in the TDD concept than I was in previous attempts, but I’m still not convinced that TDD is a golden hammer appropriate for all solutions, all the time.

I’m going to make a list of pros and cons here as I work, both for myself and for posterity. Once I’ve compiled the list, I’ll wait a little bit, reflect, and write a conclusion on it (so yes, the part at the bottom will be written two or days removed from what you’re reading right now).

Pros

  • Using mocking frameworks (Moles and Moq), maintaining 100% code coverage is effortless
  • Tests tend to be more meaningful for your use cases than retrofitted ones.
  • Better gaurding against regressions
  • TDD is better even than I remember at forcing me to think through various scenarios of method control flow and object state. I’m having far fewer cases of, “oh, I didn’t consider scenario X!”
  • Cuts down on potential for dead code. I’m not doing things like declaring a setter because I’m already declaring a getter and I might as well. The rules of the process don’t let you code that until you have a test for it.
  • Tends to help generate useful and well-conceived abstract utility classes (such as a no-exception wrapper for string splitting that I wrote) that can be reused elsewhere.

Cons

  • Larger design concepts do not seem to be emergent the way they might be in a more shoot-from-the-hip approach. Figuring out when you need a broader pattern seems to require stepping out of the TDD frame of mind
  • There is no specific element of the process that naturally pushes me away from the happy path. You wind up creating tests for exceptional/edge cases the way you would without TDD (I guess that’s not strictly a con, just a lack of pro).
  • It becomes easier to keep plodding away at an ultimately unworkable design. It’s already hard to throw work you’ve put in away, and now you’re generating twice as much code.
  • Correcting tests that have been altered by changing requirements is magnified since your first code often turns out not to be correct. I find myself refactoring tests as often as code in the early going.
  • It has the potential to slow development when you’re new to the process. I suppose one might look at this as a time investment to be paid off when you get more fluid and used to doing it.

Conclusions

I like aspects of this process, and I like it enough that I’m going to keep making stabs at it to find exactly how it works best for me. In my earlier post, I mentioned that I’d do prototyping without tests and then do TDD in earnest when I started writing my “for-real” code. I think I’m going to try to phase it in increasingly to my prototyping as well.

My biggest issue is my perception that TDD removes more emergent macroscopic principles of design. There is no straightforward refactoring I’m aware of that leads to a MVVM pattern of development or to using something like a flyweight. So it’s going to be up to me and my refinement of my own process to reconcile Uncle Bob’s three laws of TDD with sophisticated design concepts.

Of course, I’d be interested to hear, in comments, anyone’s take on how to reconcile these two concepts. Perhaps I just haven’t been at it long enough to see a test that I can write that demands a refactoring to a more complex pattern. Or, perhaps I’ve become so used to recognizing good fits for patterns ahead of time that it’s no longer part of a refactoring for me, but the original ‘factoring’. In that case, I suppose I’d have to retool my methodology for arriving at design patterns.

Regardless, though, it should be an interesting endeavor. I like to shake up my way of doing things as often as possible to look for opportunities for improvement.

  • Marc

    I’m trying here to answer your conclusion-question: as I understand it, you wonder about how to do TDD and still have a good high level design.
    Well, TDD is about sticking to the idea of “as simple as possible” functionalities. By nature, TDD make its simplier than we can thought because most of the time we overthink!

    The second idea of TDD is the emergence of the design. That is, after few, but important steps, we eventually reach that moment where something like a structure, or at least a process, emerges.

    At that moment, and even before, you never have to hesitate to refactor by first detecting bad smells and also improve your design but without changing the tests: that is the principle of TDD right? well, some tests have to be changed when they are redesigned and this is ok sure.

    Now your question rises and is justifiable: is the emergent design a real and good long-term design?

    Put in other way, am I not throwing code line after the other and making the think works and that’s all?…

    Well, making something simple has nothing to do with making something simplistic: simple is the better. The better design is the simplier, always!
    Like with theories, when two theories “do” the same things, the simpliest of both is the better.

    All of these being said, I can conclude and give a clue to answer your question by asking in return: how good is a design when, before you need it, you use it?

    Even though you think that some piece of design pattern, for example, will (!) be necessary, I think that premature design harms a software as much as premature optimization and, it is always time to “redesign”… we simple call that refactoring in TDD.

    Design patterns are great but let the need commands the use of them may be the idea of TDD…

    What do you think ?

  • http://www.daedtech.com/blog Erik Dietrich

    It’s interesting for me to read back through this post 2 years later, as I really no longer identify with any of the cons. It turned out that most of my speculated objections just never wound up applying. If I’m doing something broader, architecturally or a design pattern or something, I just decompose it and test the components as I build them.