About this post
I had intended to write a post about software craftsmanship in general. Instead, the topic of test-driven development/design (TDD) seems to have taken over.
The irony isn’t lost on me. You don’t always know at the outset of creating something what form the final product will take. TDD is a good fit for this: you’re not guessing what you might need, just creating the next small thing you know you need in the simplest way possible. The final product can look after itself.
Test-driven design (TDD)
Degrees of hatred and fandom
How do you feel about unit testing? It seems to be a topic that divides developers.
I’ve always been a fan of having at least some unit tests, but over the last few years I’ve moved from “use common sense and write just enough good tests afterwards” to “use strict test-driven design”.
Everyone fits somewhere on the scale between “writing any tests at all is a complete waste of time” to “all code should be written using TDD”. The purpose of this article is to encourage you to nudge to the right on this scale; at least try the next step and see how good a fit it is for the projects you work on.
Uncle Bob Martin defines the Three Laws of TDD as follows:
You are not allowed to write any production code unless it is to make a failing unit test pass.
You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
His Clean Coders videos give a good and entertaining introduction to TDD and other important topics in this area. The key point is that you are constantly in a tight loop: red (failing test), green (passing tests), refactor.
The three rules above are simple but they are not simplistic. It takes practice to write code in this way. If you’ve been writing code for years, following these rules seems unintuitive, even patronising. It’s almost as if they were designed to slow you down.
If you only do manual testing, or no testing at all, then you are doomed to a lifetime of chasing bugs in production code. You’re climbing a mountain without a safety rope.
Seriously, if you think TDD is a waste of time and effort, or something that’s only useful for newbie programmers, I’d encourage you to try it out on your next pet project. (If you don’t have a suitable project coming up, try Roy Osherove’s String Calculator TDD kata.) I think you’ll be pleasantly surprised by the positive effect it has on the code under test and how many times you’ll find you’ve uncovered a bug during development that would otherwise have made it into the wild. However, it does require a degree of humility to get started; a willingness to accept that, just maybe, there’s a better approach than the one you’ve been using for years.
Killing disease at the point of contraction
How unit tests stop bugs
It’s all very well saying that TDD helps guide the design of the product, but you probably care more about what it can do to help prevent bugs. After all, the tests are there to check behaviour and uncover bugs.
TDD has a dual role when it comes to finding bugs. Firstly, as part of the process, you’re continually writing new tests for behaviour that doesn’t yet exist, then modifying the code until the test passes. Sometimes you’ll run the new test, expecting it to pass, but it fails. This is a genuine bug; because you’ve caught it at the point of injection, it’s probably easy to fix. Wipe your brow and move on, but remember to be thankful that the test was there. Just think how long it might have taken to track the problem down from a customer’s patchy bug report six months after deploying the code!
The second role TDD plays is in the construction and frequent running of a comprehensive suite of regression tests. If you break something, you should find out straight away. That’s very powerful.
Avoiding bugs altogether where you can
Testing isn’t a silver bullet. All useful code has a non-zero bug density.
But code that doesn’t exist does have zero bugs. You just can’t get better than that! It’s why principles such as DRY (don’t repeat yourself) and YAGNI (you aren’t gonna need it) are important: having code once is better than having it twice; having dead code rotting alongside living code is harmful and confusing.
Poor developers write buggy code. Better developers write fewer bugs. Great developers avoid writing the code at all.
If there are bugs, finding them early
Use automated methods to catch simple problems first. Then use code review to catch even more. There are some fine static analysis tools that you can run over your code to find problems early, and to avoid code reviewers from being bogged down with finding issues that could have been caught by a tool:
Finding bugs early is also why I prefer strong typing over dynamic typing. I’d
rather find bugs at compile-time than at run-time, and I’ve never wanted to
string as an
int in mid flow.
It all comes back to simplicity.
Source code has a natural tendency to become more complicated over time. Hacks and other special cases will be added. Documentation will get out of sync with the code. A dozen people will edit the same file, applying subtly different coding styles to it each time. Classes will grow ever bigger and take on more and more responsibilities. Some code will become uncallable or uncalled and start to rot.
It takes energy to fight against this complication.
TDD can help. Constant, safe refactoring is a force for keeping the code simple. Making just one focused change at a time is working in a simple way.
Static code analysis can also help. It helps to enforce good practice across the code base, which makes its maintenance more straightforward. Some level of automated analysis eases the burden on code reviewers, so they can concentrate on the more complex issues that can’t be found automatically.
I believe that these three quality gates – the use of TDD to guide design and create a regression framework, static analysis to find some problems automatically and code review to find still more – all help to minimise the number of bugs escaping into the wild. Which leaves developers with more time to do useful work and less time chasing their tails fixing bugs in production code.
* Unit tests prove that your code actually works * You get a low-level regression-test suite * You can improve the design without breaking it * It’s more fun to code with them than without * They demonstrate concrete progress * Unit tests are a form of sample code * Test-first forces you to plan before you code * Test-first reduces the cost of bugs * It’s even better than code inspections * It virtually eliminates coder’s block * Unit tests make better designs * It’s faster than writing code without tests! ↩