Test-Driven Development: I Finally Get It

I’ve never been a big fan of TDD (“Test-Driven Development”). Some things are just plain hard to test. Some people end up doing end-to-end tests with tear-down temporary databases and calling them “unit tests”. Recently I’ve seen the light and realized my mistake. I think I’ve unfairly misjudged TDD but at the same time I think it’s made a serious error.

Let’s back up. Kent Beck’s 2002 book Test-Driven Development By Example ignited this particular movement (although it seems to be more of a lit brazier than a raging bonfire to take that metaphor further). As Beck describes it:

My goal is for you to see the rhythm of test-driven development:

  1. Quickly add a test
  2. Run all tests and see the new one fail
  3. Make a little change
  4. Run all tests and see them all succeed
  5. Refactor to remove duplication

This is the part I’ve always had a problem with.

Not Everything Needs To Be Tested

This is my first criticism. Now I don’t think any TDD advocate will argue that absolutely everything needs to be tested. After all, load a controller in your Spring MVC application and it’s either there or it isn’t.

But my point is that that the premise of testing everything strikes me as naive.

Not Everything Can Be Tested

Wikipedia has a good summary of TDD Limitations. The one that jumps out at me is UIs. That’s a bit of a problem since a lot of software has UIs. What’s worse is that they’re increasingly Web UIs that are even harder to test because the functionality can rely on server calls and once you start getting into mocking Ajax calls it’s not long before you’re banging your head against the wall hoping the pain goes away.

Unit Tests Are Fragile

Often if you get a decent suite of unit tests you are only a few requirements changes or change requests away from half of them breaking. Unit tests are software. That may sound trite but it’s worth remembering. The larger and more complex your unit tests, the more code you’ve written to implement a particular feature (the tests count in that metric) and that’s more code you have to change.

I’ve lost count of the number of times I’ve seen build instructions that have quickly degenerated into:

mvn –Dmaven.test.skip package

I See The Light

The problem with Beck’s methodology is that it is prescriptive. Write a test, fail it, write some code. I see this as a useful learning technique but I don’t think it should be the end goal.

Let me give you an analogy: if you describe Spring to a novice Java programmer or one that is simply unfamiliar with dependency injection or inversion of control they just don’t get it.

That’s completely understandable. I can still remember when I first heard about Spring back in 2004 and I didn’t get it. It just sounded like a way of standardizing configuration for, say, EJB containers (EJB 2.x having different config files and behaviour depending on which application server you used). In that context it sounded potentially useful but nothing to get too excited about. Statements such as “Spring is a lightweight container” just didn’t sink in.

Of course, that just misses the point completely.

Having learnt the error of my ways, I now firmly believe that DI/IoC are now right up there with object-oriented programming and managed code as key turning points in software development. The point is that it changes how you design software, how you construct your object model and how you put it all together. In doing so you should realize (if you hadn’t worked out already) just how evil an anti-pattern the static Service Locator really is, which was common in J2EE applications 5+ years ago.

One of the benefits of Spring is that an application designed with Spring in mind will in all likelihood have much greater testability as anything with injected behaviour can just as easily have mock objects injected instead.

What about TDD?

The conclusion I’ve come to is this: TDD is descriptive not prescriptive.

Put another way, TDD is first and foremost a set of principles rather than a rigid methodology. Just like Spring (and somewhat related to it) TDD for me is about changing your thinking.

I’m currently in the process of writing a Java library I hope to release as open source soon. As I wrote in It’s Time We Stopped Rewarding Projects with Crappy Documentation: Open Source is No Excuse, I firmly believe that any publicly released software—open source or otherwise—must be held to a higher standard than software you may just write for yourself. For my upcoming library this means having extensive unit tests with high coverage.

That’s the key point: before I wrote a line of code, while I was designing it (in my head) I was asking myself “How do I test this?” For some of it I wrote unit tests first. For some of it I haven’t. But the point is that I’ve thought about it from the beginning.

You can contrast that with software I’ve written previously where unit testing has been nothing but an afterthought at the end of the project, something to do to check off the “unit testing” box on the project plan. In my experience, that approach works incredibly badly. The longer you put off thinking about how to write automated tests for something, the harder it will be to graft in later.

Conclusion

My view of TDD can be summarized as follows:

  1. Think about how to write automated tests for something before you start; and
  2. TDD is descriptive not prescriptive.

Purists may argue this isn’t TDD at all. If not it is at least the essence of TDD. There’s no need to be a fanatic about unit testing. Like so many other things, it’s just another tool in your hopefully extensive toolbox.

7 comments:

mudboy said...

I feel you have missed the essence of TDD. The test are not so much about testing your code as forcing you to think about how to design the interface to the code.

A natural consequence of this is it forces the design of the code to be less coupled and more cohesive. This will also naturally lead to the use of techniques like DI/IoC (it allows mocking of dependences).

I feel Test is a bit of a misnomer, Behaviour Driven Design(BDD) is more like the true intent.

BruceA said...

Thanks for that insight. I too have struggled for a long time to "get" TDD, and now I think it finally makes sense.

mudboy -

Surely you're not responding to this post. Didn't he say the essense of the TDD is the very same thing that you claim he missed?

David Bland said...

Commenters above: I'd be careful swapping BDD with TDD. For example, Cucumber (www.cukes.info) serves a different purpose.

Ashley Moran said...

BruceA - I don't think mudboy is misinterpreting the article. William's post does not describe TDD as a process of defining the interface and behaviour of an article, but one of determining the structure of the code to allow tests to be written easily.

The re-run (regression-prevention) benefit of BDD is actually a side effect. The initial goal of BDD is to write an executable specification for code that allows a simple, and hence maintainable, OO implementation. As it happens, the fact you can re-run these over the lifetime of the project is probably of more value (or at least prevents more waste) that the initial design, although it depends on the lifetime of the code.

Having done several years of BDD coding, I can write clean OO code _much_ faster now, even if I don't write specs. If it's throwaway code that I'm likely to get right first time, that's fine. But if I don't understand the problem, or it's going to to evolve over a long time, the value of an executable spec becomes quickly apparent - but for different reasons.

"Often if you get a decent suite of unit tests you are only a few requirements changes or change requests away from half of them breaking" is not, in my experience, true. If a small number of requirement changes cause this much damage, it's likely one or more OO principles have been violated, possibly Single Responsibility, or a concept that is not firmly embedded in a class or method of its own.

On the other hand, I don't entirely disagree with "Think about how to write automated tests for something before you start". I'll accept it if is intended to mean "define the behaviour of the code you're about to write in your head before you touch the keyboard". However, I know I'd have to be superhuman to actually do that in practice, for any non-trivial piece of code. YMMV...

ashley Moran said...

David - why do you think BDD is different from TDD? (And not an evolution of the same thing?) I use them synonymously, although I avoid TDD unless I'm replying to someone that uses the term themself.

Curtis Cooley said...

TDD is a developer activity, BDD is a customer activity. BDD is a way to write executable requirements. The customer writes requirements/stories using a BDD tool and those stories can actually be executed to 'prove' the feature works.

TDD is a design activity performed by the developers during 'construction'.

Ashley Moran said...

Curtis - the line you're drawing is not clear, to me at least. After all, developers are the customers of other developers' code. Is there any reason why the same principle of meeting the requirements/business needs of the customers doesn't apply? (The activity you describe as "TDD" I refer to as "writing code".)

Post a Comment