A brief look at the logic of TDD

[Test] 
public void Should_do_tdd() { 
  Assert.That(me.ShouldTdd, Is.EqualTo("Not sure?"));
}

Jacob Proffitt has a post questioning whether TDD provides any benefits over Plain Old Unit Testing (POUT). POUT itself has many benefits, many of which became apparent to me reading Michael Feather’s Working Effectively with Legacy Code. In the comments on Jacob’s post I said I thought it unlikely that TDD would be proved better than POUT, due to problems measuring, and even defining, software quality. Jacob replied that he wasn’t interested in a proof, but was simply after the reasons why TDD might help. So I thought I would have a quick run through the logic of why TDD might help you write better software. I’m going to steer away from benefits of TDD that you can also get from POUT (like low coupling/high cohesion etc), and focus on the my interpretation of the rationale behind TDD.

Please note: I am not trying to convert anyone. I firmly believe that you should use what works for you. If a particular tool doesn’t help you, don’t use it. If you can’t see any value from an approach and feel it is a waste of time to examine further, don’t try it. I’m also far from an expert on TDD. But seeing as Jacob took the time to reply to my comment, I thought I should at least return the favour :-)

Quick TDD review

TDD is design tool/process. You write a test first that describes some behaviour that your production code should exhibit. You run the test and it fails because you haven’t implemented that behaviour yet. You then write some minimal code that makes the test pass. Finally, you refactor the code to remove duplication and improve the design, re-running the tests to make sure you haven’t broken the behaviour. This process leads to the TDD slogan: “Red, Green, Refactor”, red and green being the colours of the status bar in most xUnit test frameworks to show failure and success.

So what?

What is the logic of that? Testing code that doesn’t even exist yet? Crazy!

Well let’s start with the obvious stuff. First up, you get unit tests. Unit tests are good, and let you refactor safely. POUT/test last obviously does this too. You get quick feedback on whether the code you have just written breaks anything. You can do this with a POUT approach as well, depending on how quickly you write your tests, or whether you use manual testing for feedback. TDD provides a bit more assurance that you will get the quickest possible feedback and will always have a decent amount of code coverage, but with sufficient discipline a POUT approach can accomplish all of this. And we trust our development team right? We don’t need some process to force them to do the right thing.

TDD as a design tool

TDD is a tool for incrementally improving the design of your code. The unit testing side of it is simply a nice side effect, to the point where BDD has been proposed as an alternate presentation of the technique to eliminate the apparent confusion caused by the word “test”. So how can TDD be used to improve design?

Well, first up you are specifying the exact behaviour you want to implement before writing the code to do it. This has several effects. You are writing the logical interface to your class before the class itself (interface, not public interface IInterface{}). This takes some of the guesswork out of determining how your code is going to be called. You have to deal with the interface to your class in order to test it, so if it is painful to use you can immediately tell and do something about it. According to my pseudo-logic, this can help ensure production code that needs to talk to your object should have a well designed interface through which to do so easily.

Specifying the behaviour you want first also helps focus you on one piece of code at a time. You are just trying to pass the test, not immediately trying to solve every problem the class will ever face. This can help reduce feature creep in your design. If you find that a “divide and conquer” approach can make problem-solving easier, then isolating the one thing you want to achieve in this way can also make coding easier.

If you have never written a class that exposes an interface that turns out to be fairly unusable in Real LifeTM, or if you have never worked for a while getting your class to handle a situation that will never actually occur (e.g. “What if this class needs to take a DateTime as an input instead of just ints?”, followed by a large effort to produce a generic version of your class that is only ever called as MyClass<int>) then you are a much better developer than I (probably a given), and you may not get any benefit from TDD. TDD doesn’t prevent these problems, but it can help make these problems immediately apparent and therefore potentially avoidable.

The refactoring step is an essential part of the design aspect of TDD. The idea is that you have just gotten your test to pass, so your class’ behaviour for that specific test is correct. The very next step is to improve the design. Remove any duplication, extract any methods or rename variables as required to make the code more readable and understandable. Sprout any new classes that are required to better encapsulate the data or behaviour. Refactoring is not unique to TDD, but TDD helps you to perform these design improvements in small, hopefully easy, increments. If your test is difficult to write or implement, this feedback tells you your design might need tweaking, or that you initial specification needs more clarity. Your design is evolving based on continuous feedback from your immediate requirements and your actual code. This can help avoid large, difficult refactorings and unnecessary refactoring.

By improving the design at each step, in theory you make the next step easy to perform. The goal is having code that is always easy to change, making your more responsive to changes in business needs and requirements, and making bugs easier to fix (that’s right! You still get bugs doing TDD! :-P).

Useless point on the “magic” of TDD

Aside from my convoluted version of the theory behind TDD, I feel TDD has the potential to change the way you think about coding. For me TDD had a profound effect on how I think about software design, to the point where even when not doing TDD I still find myself thinking in terms of passing tests and getting feedback from incremental coding steps. I feel this has made me a better developer, providing me with another way of thinking about problem. The extra perspective may not help you in every situation, or ever, but it does give you another avenue to pursue when you are stuck.

I also find I come up with much cleaner designs using this different perspective. The Robert Martin’s Coffee Maker example [PDF] illustrates some OO design traps. In my experience the incremental design encouraged by TDD helps avoid some of those traps. Of course, this is all anecdotal hand-waving, hence this section’s title :-)

Clarifications and conclusions

Refactoring, incrementally improving design, isolating behaviour, writing testable code, etc can all be achieved without TDD. TDD is just a tool to help you do all these things. If you do them fine without TDD, then TDD is probably not going to help you. If the technique sounds interesting to you, then your best bet is to give it a try and see if you see any benefits over and above POUT (I first got into TDD following this example. I converted it to C# and NUnit, commented out all but one test, and started coding).

I have not listed anything that is a dramatic, inspiring benefit of TDD. That is because TDD is simply a tool to help achieve what we are all trying to do anyway: writing good, well designed software. If you do that already then TDD may just seem like more work. This is probably why people that feel passionately about TDD can have a hard time selling it to people that are skeptical of it. It isn’t dramatic. There is no “if you do this you will become almost as popular as Justice Gray” aspect to it.

I should also be clear that, for me at least, TDD did not come easy. I am definitely still learning, but my confidence and my results improve the more I use it. I feel it has been worthwhile. YMMV. If you have a mentor or a colleague to learn with this process may be easier.

As a closing aside, I think everyone will agree that at least people are debating the merits or TDD vs. a simple, good bank of units tests, rather than trying to justify the practice of unit testing itself. :-) Hopefully that means I won’t ever have to face the “We don’t have time to unit test” discussion again ;-)

Comments