Clarification: this isn’t a post about BDD vs. TDD, it’s a post about Spike and Stabilize. Tagging those now.
For love of TDD
Dan North and I have been talking about some different ways of writing software that matters. We’ve been talking about not doing TDD, and not automating BDD scenarios.
A lot of people have reacted to these suggestions with something approaching visceral horror, as if we’ve committed outright heresey, which I guess we have. TDD is such a game-changing practice that those who’ve discovered it recently may not be able to even imagine a world without it, since that would mean going backwards. So I want to explain a little bit about my take on TDD, just to make it really clear, and then introduce some different ideas.
TDD is amazing.
TDD is a truly important practice that every developer who writes code for a living ought to know about and attempt to master. TDD is how I learnt to write maintainable code; design classes with appropriate and single responsibilities; understand what it was that I was really trying to achieve.
The people who came up with TDD are deserving of every respect, which I haven’t always given them. I decided to start with that this time just to make it clear. TDD is a flippin’ awesome practice. When we talk about “BDD isn’t just TDD”, it definitely evolved from it. If TDD hadn’t existed, BDD and ATDD wouldn’t have either. So a big, massive thank you to all those people who helped create and espouse the values of TDD.
If you haven’t experienced really great TDD, stop reading. This post is not for you. Go read Bob Martin‘s many articles and posts, or Kent Beck’s book, or find your local code retreat instead. You will get more out of that time than reading this article.
Dan North and I have been talking about moving beyond TDD for a while. Here’s a confession: I don’t always write my tests first. I often don’t automate scenarios up-front either. And I consider it right for me not to do so.
Dan recently blogged on the opportunity costs of our various practices, and used TDD as an example of a practice that carries such a cost. It can take longer to produce software with TDD than without. I know this because I’ve tried it both ways, as has Dan. We do something that Dan calls “Spike and Stabilize” – trying one or several things quickly to get feedback, then stabilizing the end result after we’ve managed to eliminate some of the uncertainty around what we’re doing. I can’t tell the difference between the end result when I TDD something to when I spike and stabilize, except that the second one lets me get feedback from my stakeholders faster, and is easier to change in response to that feedback.
For me, that means starting with a UI and knocking out something approximate, frequently with hard-coded data. If the UI is the place of uncertainty, I’ll show it to the stakeholder immediately, or try it a couple of ways. Sometimes it might be a performance concern, in which case the UI will be something that exercises the performance. Sometimes I might need some simple behaviour behind the UI, which I also just “hack out”.
I keep all the code I’m writing clear, readable and easy to change. I can do this because I can write examples of how my class is going to work (a.k.a. unit tests) in my head. That’s come from hours of doing TDD, at work, at home, at code retreats, after work, during lunch hours. If you want to be really, really great at coding, I think you do need to have the 10,000 hours of practice that Malcolm Gladwell talks about. I’m pretty good rather than really good (and particularly, I have shallow rather than deep knowledge) but I get TDD enough to move beyond it. I’m emphasising this not to show off my knowledge, but to stop anyone who’s still learning TDD from thinking, “Oh, this is great! I can just do what Liz does.” Get good at TDD first.
Dan works with amazing people who are way better than I am. He’s damned good too. Developers like Dan and his team don’t need TDD in order to create good designs, separate responsibilities or simple code that does just enough to do the job. They’re really good at TDD, and have moved beyond it.
For most people, TDD is a mechanism for discovery and learning. For some of us, if we can write an example in our heads, our biggest areas of learning probably lie elsewhere. Since ignorance is the constraint in our system, and we’re not ignorant about much that TDD can teach us, skipping TDD allows us to go faster. This isn’t true of everything. Occasionally the feedback is on some complex piece of business logic. Every time I’ve tried to do that without TDD it’s stung me, so I’m getting better at working out when to do it, and when it’s OK to skip it. If in doubt, I do TDD. I recommend this as a general principle. Otherwise, I try to eliminate the ignorance I have. Here are the rules I’ve learnt which help me to spike things out and stabilize them.
- If in doubt, do TDD.
- If it’s complex enough or has enough learning in it that pairing is useful, do TDD.
- When spiking, hard-code the context rather than adding behaviour wherever possible. Hard-coded data doesn’t require TDDing.
- Don’t pick the right libraries or technology. Pick the technology that’s easy to change. And don’t write tests around other people’s libraries – isolate them through interfaces or adapters instead.
- Once the uncertainty you’re dealing with is no longer the area of greatest ignorance, stabilize by adding tests. You’ll be able to tell at this stage if you should have TDD’d first based on how much rework you have to do to make the tests readable.
- Adding tests afterwards is especially important if you’re working in a team with mixed skills or silo’d roles, or other teams in the same code-base, as your team-mates and colleagues will need them to provide documentation.
- Adding tests afterwards is especially important if you’re the kind of person who forgets what it was you were working on after a couple of months (i.e.: just about everyone).
- Adding tests afterwards requires even more discipline than adding them before-hand. If you find you’re under pressure to skip that step, do TDD instead.
- No amount of automated testing is a substitute for trying out the code you’ve just written manually.
- If your user is another system, hack something out to allow you to pretend to be that other system, then stabilize that. You’ll need it later.
By doing TDD – writing our tests first, or before we’ve got feedback on what it is we’d like to achieve – we are creating a premature commitment. The principles of Real Options say, “Never commit early unless you know why”. If you’re working with uncertainty, and if you can avoid adding investment before getting feedback – for any practice – then you will actually go quicker and be able to respond to discovery faster. You will be more agile.
However, if you don’t yet know how to write software that’s easy to change and maintain, every piece of code you write is a more concrete commitment than it will be if you learn to do TDD, since TDD helps you to adopt these practices, as well as providing nice documentation and living examples of how to use your code, how it behaves and why it’s valuable. If you haven’t yet experienced amazing TDD and you try doing things this way, you’ll probably find that you end up with a lot of rework when you come to stabilize the spike (and if you don’t stabilize the spike, you’ll eat up cost in maintenance later).
It’s interesting to me that this is actually how a lot of people learn to do TDD – by adding tests afterwards, until they gain enough confidence to add tests first. It really does remind me of this quote from Bruce Lee:
Before I studied the art, a punch to me was just like a punch, a kick just like a kick. After I learned the art, a punch was no longer a punch, a kick no longer a kick. Now that I’ve understood the art, a punch is just like a punch, a kick just like a kick. The height of cultivation is really nothing special. It is merely simplicity; the ability to express the utmost with the minimum.