Test Driven Development - What is it good for?

Friday 30 January 2009

Abstract:
  • This post is an introduction to TDD and its impact on software projects
  • Test Driven Development is about writing your unit tests before you code, letting the tests act as a driving force of how your code is shaped.
  • TDD is closely coupled with the practice of Refactoring. Having a test suite in place enables you to make refactorings at the project level while capturing and fixing the functionality that gets broken by this.
  • The need to write isolated Unit Tests forces you to lower the dependency between your components. This makes code easier to reuse and understand.
  • The big benefits of TDD are of a long term nature but requires an initial investment of time. Sacrificing practices such as TDD, that aim to improve project quality, incurs technical debt that you will eventually have to pay.

So what is TDD? Here comes an overview and some thoughts about it.
This is ground that is already pretty well covered elsewhere, but I'm sure someone will find this useful or interesting.

What is Test Driven Development?
TDD is the act of writing unit tests before you write code. When adding a new feature you follow these steps:
  1. Red - Write a Unit Test that exercise functionality that you want to exist before you create the classes or methods that will fulfill that functionality. This will probably result in your project not compiling. With the least possible effort you can, make it compile, then run the test and see it fail.
  2. Green - Make your test pass by modifying the classes and methods you're testing. Do not add any functionality that is not required to make the test work. If your current test does not test a desired piece of functionality make a note of it and write another test later.
  3. Refactor - Since you've only done what is necessary to make your test work, chances are that your code is not very pretty. Refactor the code to make it prettier, remove any duplication you can find etc, and run your test again to verify that the functionality you wrote the test for is still in place.
This page goes into more detail, has some pretty diagrams and many wise things to say about it.

What are the benefits of Test Driven Development?
Kent Beck in the first sentence of his classic book defines the goal of TDD as: Clean code that works
This really sums it up quite nicely.

Clean code
Since the unit tests drive the production code the code will only contain the functionality dictated by the tests. By adding only the necessary code in small increments we avoid creating a system that is overly generalized, containing speculative functionality that is designed before we're sure there's an actual need for it.
Now, if you're working in a big project you might find this way of doing things worrying. You may think that going into a project using no upfront design at all is surely a way of getting a tangled mess designed to work for the benefit of a short term solution to a series of isolated problems. This argument has merit, and here are a few important aspects to consider about how TDD and evolutionary design impacts project architecture:

Refactor
the third step of RGR must not be forgotten. The design of software architecture is a tricky business. When you start a project you will do well to have a general structure that dictates where new classes will be created, and common problems often have well known solutions that are readily applicable. More often than not though, design is something you discover in your code as a project grows. You may find that duplication of code leads you to create common classes and superclasses to enable reuse and eliminate duplication. Having a suite of unit tests covering the requirements of your existing functionality enables you to change the structure into something that is more elegant and works better. Large refactorings at the project level can be done with a greater degree of confidence if you have tests that will fail if a piece of functionality suddenly gets broken from a change. 
And how do you ensure that the unit tests will cover the existing functionality and catch errors with large refactorings? Why by adhering strictly to the principle of not adding code that is not covered by unit tests. That is easier said than done as unit tests can be cumbersome to write, but keep in mind that the benefit they bring is not only at the micro level for the specific functionality under test, but at the macro level enabling you to do some pretty funky stuff with your project architecture as development progresses.

Lowering dependencies
A Unit Test should by definition test a small unit of functionality. This is tricky. Classes tend to have dependencies on other classes, that in turn depend on other classes still. You may be able to create an instance of a Business Object without creating an instance of anything else, but what about cases where you want to test a method combining logic that spans several BOs, calls to the database, calling remote APIs and websites, or simply accessing the web context not available in a test?
The requirement to be able to write these kinds of unit tests without touching on external systems forces you to construct your architecture in a way that is loosely coupled, programming towards interfaces and abstractions rather than the actual objects themselves. Classes should move towards only having one well defined responsibility which increase the cohesion between the classes of the system and makes them part of an easy to understand, powerful whole that is easy to weave together and reuse.

...that works
The most first benefit that springs to people's mind when they hear the word Unit Testing, is the verification that the system actually works correctly. As I have tried to outline above, the big impact of TDD on a project is more extensive than that, but the fact remains that the immediate result you will see from writing a unit test is that the code you have written works in the way outlined by the test. 

What is the cost of TDD and when is it not worth paying?
So, in summary: Test Driven Development is the dog's bollocks and should always be used in all projects. Thank you for reading and... HEY! What about the downside? Surely there is a price to pay?

There is, and the price is time. And time is money. Writing tests before you write code usually takes more time initially than just writing code. TDD is something that requires a commitment at the team level in order to reap the full benefits from it, and initially you may have to spend time in getting your team members up to speed.

Benefit talk again: The time spent on writing unit tests is time spent on building an asset: Your test suite. The gain is a long term one in the ability to refactor with confidence, fight software rot, reduce the number of bugs in the system and the time spent looking for them. As is the way with investments, you pay a high price in time initially to free up time later. 

Quick and dirty - There is another kind of investment you can make: paying a small price in time initially with the consequence of having to invest more time later. There are numerous occasions on which this makes perfect sense. The trick is to know when those are. 
The bottom line for software success is judged by the fulfillment of business requirements. Having a gorgeous test suite and fantastic structure but an unfinished system makes no sense at all. If the goal of the project is to produce something that is better than nothing in a short time you need to make compromises with the rigidity of practices you apply.
However, having an evolutionary project stagnate under software rot due to an endless series of quick fixes and shortcuts makes equally small sense. The concept of technical debt is a powerful metaphor for reasoning about these things. Don't take a loan if you can't afford to eventually pay it back.

The road to hell is paved with good intentions
To apply TDD can be difficult and there are numerous things you can do with it that puts you in a straitjacket rather than giving you wings. It is quite possible to device inefficient tests that take ages to construct and ages to run, while providing minimal benefit. This however is a question of implementation and I will leave it for another day.

So, in summary: Test Driven Development is the dog's bollocks, but should be applied as part of an overall strategy in projects where the aim is to build solutions of high quality that are easy to maintain and expand.

I suspect that for a large number of people this post has been preaching to the choir, but it would be interesting to hear more opinions and experiences of when TDD is not worth it. My own views are tainted by my very positive experience of TDD so I am bad at taking the other side of the argument. Drop a comment either way!

0 comments:

Post a Comment