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:
- Quickly add a test
- Run all tests and see the new one fail
- Make a little change
- Run all tests and see them all succeed
- 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:
- Think about how to write automated tests for something before you start; and
- 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.