- It's more work. I'm already over-burdened and now you're giving me a new job to do.
- I'm not a tester. We have testers for testing, and they have more expertise than I do. It will take me a long time to learn how to write tests as well as they do.
- If I write the code, and then test it, the test-pass will only tell me what I already know: the code works.
- If I write the test before the code the failing of the test will only tell me what I already know: I have not written the code yet.
This is an understandable concern, at least at initially, and it is not only the developers that express it. Project managers will fear that the team's productivity will decrease, which they are accountable for. Project sponsors fear that the cost of the project will go up if the developers end up spending a fair amount of their time writing tests. The primary cost of creating software is developer time.
The fact is, TDD is not about adding new burdens to the developers, but rather it is just the opposite: TDD is about gaining multiple benefits from a single activity.
In the test-first activity developers are not really writing tests. They look like tests, but they are not (yet). They are an executable specification (this is a critical part of our redefinition of TDD entry). As such, they do what specifications do: they guide the creation of the code. Traditional specifications, however, are usually expressed in some colloquial form, perhaps a document and/or some diagrams. Communication in this form can be very lossy and easy to misinterpret. Missing information can go unnoticed.
For example, one team decided to create a poker game as part of their training on TDD. Often an enjoyable project is good when learning as we tend to retain information better when we're having a good time. Also, these developers happened to live and work in Los Vegas. :) Anyway, it was a contrived project and so the team came up with the requirements themselves; basically the rules of poker and the mechanics of the game. One requirement they came up with was "the system should be able to shuffle the deck of cards into a reordered state." That seemed like a reasonable thing to require until they tried to write a test for it. How does one define "reordered?" One developer said "oh, let's say at least 90% of the cards need to be in a new position after the shuffle completes." Another developer smiled and said "OK, just take the top card and put in on the bottom. 100% will be in a new position. Will that be acceptable?" They all agreed it would not. This seemingly simple issue ended up being more complicated than anyone had anticipated.
In TDD we express the specification in actual test code, which is very unforgiving. One of the early examples of this for us was the creation of a Fahrenheit-to-Celsius temperature conversion routine. The idea seemed simple: take a measurement in Fahrenheit (say 212 degrees, the boiling point of water at sea level), and convert it to Celsius (100 degrees). That statement seems very clear until you attempt to write a unit test for it, and realize you do not know how accurate the measurements should be. Do we include fractional degrees? To how many decimal places? And of course the real question is what is this thing going to be used for? This form of specification will not let you get away with not knowing because code is exacting like this.
Put another way, a test would ask "how accurate is this conversion routine?" A specification asks "how accurate does this conversion routine need to be" which is of course a good question to ask before you attempt to create it.
The first benefit of TDD is just this: it provides a very detailed, reliable form of something we need to create anyway, a functional specification.
Once the code-writing beings, this test-as-specification serves another purpose. Once we know what needs to be written, we can begin to write it with a clear indication of when we will have gotten it done. The test stands as a rubric against which we measure our work. Once it passes, the behavior is correct. Developers quickly develop a strong sense of confidence in their work once they experience this phenomenon, and of course confidence reduces hesitancy and tends to speed us up.
The second benefit of TDD is that it provides clear, rapid feedback to the developers as they are creating the product code.
At some point, we finish our work. Once this happens the suite of tests that we say are not really tests (but specifications) essentially "graduate" into their new life: as tests, in the traditional sense. This happens with no additional effort from the developers. Tests in the traditional sense are very good to have around and provide three more benefits in this new mode...
First, they guard against code regression when refactoring. Sometimes code needs to be cleaned up either because it has quality issues (what we call "olfactoring"[1]), or because we are preparing for a new addition to the system and we want to re-structure the existing code to allow for a smooth introduction of the enhancement. In either case, if we have a set of tests we can run repeatedly during the refactoring process, then we can be assured that we have not accidentally introduced a defect. Here again, the confidence this yields will tend to increase productivity.
The third benefit is being able to refactor existing code in a confident and reassured fashion.
But also, they provide this same confirmation when we actually start writing new features to add to an existing system. We return to test-as-specification when writing the new features, with the benefits we've already discussed, but also the older tests (as they continue to pass) tell us that the new work we are doing is not disturbing the existing system. Here again, allows us to be more aggressive in how we integrate the newly-wanted behavior.
The fourth benefit is being able to add new behavior in this same way.
But wait, there's more! Another critical issue facing a development team is preventing the loss of knowledge. Legacy code often has this problem: the people who designed and wrote the systems are long gone, and nobody really understands the code very well. A test suite, if written with this intention in mind, can capture knowledge because we can consider it any time to be "the spec" and read it as such.
There are actually three kinds of knowledge we need to retain.
- What is the valuable business behavior that is implemented by the system?
- What is the design of the system? Where are things implemented?
- How is the system to be used? What examples can we look at?
So the fifth benefit is being able to retain knowledge in a trustworthy form.
Up do this point we've connected TDD to several critical aspects of software development:
- Knowing what to build (test-first, with the test failing)
- Knowing that we built it (turning the test green)
- Knowing that we did not break it when refactoring it (keeping the test green)
- Knowing that we did not break it when enhancing/tuning/extending/scaling it (keeping the test green)
- Knowing, even much later, what we built (reading the tests after the fact)
All of this comes from one effort, one action.
And here's a final, sort of fun one: Have you ever been reviewing code that was unfamiliar to you... perhaps written by someone else or even by you a long time ago, and you come across a line of code that you cannot figure out. "Why is this here? What is it for? What does it do? Is it needed?" One can spend hours poring over the system, or trying to hunt down the original author who may herself not remember. It can be very annoying and time-consuming.
If the system was created using TDD, this problem is instantly solved. Don't know what a line of code does? Break it, and run your tests. A test should fail. Go read that test. Now you know.
Just don't forget to Crtl-Z. :)
But what if no test fails? Or more than one test fails? Well, that's why you're reading this blog. For TDD to provide all these benefits, you need to do it properly...
[1] We'll add a link here when we've written this one
Excellent post, but you forgot one (or possibly more) benefit of TDD: the art of doing less. I really like your specification suite terminology, but as we are writing the 'spec' tests and just making those pass, we also avoid the common developer practice of writing code that isn't needed. If I restrict myself to just writing code to make the spec's pass, I greatly reduce (perhaps eliminate?) the hooks, extra objects, layers, etc that tend to just clog up the code, make it more fragile, and make it harder to maintain. Plus, these extra lines of code are usually the ones that become the gotcha's in production!
ReplyDeleteI completely agree, and appreciate the addition. You'll note the "at least" part of the blog title. We are always hoping readers will contribute, as you have here. Thanks!
Delete