A ditty on unit testing
Confession of the week: I rarely write unit tests… And when I write them, it’s usually only a few common and corner cases…
Blasphemy! Of course, I have good reasons for that:
- I’m lazy (as someone holding a degree in mathematics ought to be);
- I don’t write software with bugs in (yeah, right! :));
- I work hard at perfecting my code to be obviously right, not just without obvious bugs (see also point 2);
- I find writing unit tests for my own code a case of “Wij van WC-Eend adviseren: WC-Eend!” (OK, you have to be familiar with the Dutch language and television commercials for that one).
(I’m pretty sure there is a nice quote in the spirit of point 3 by the likes of Donald Knuth or Edsger Dijkstra, but I can’t find it.) While the first two reasons are obviously rubbish, the last two reasons are quite serious.
Use the source, Luke!
Regarding point 3: I’m very much of the opinion that source code is meant to be readable for humans and not for computers (as long as the compiler doesn’t complain, obviously). That means that it has to be readable at least for yourself (I’m assuming you’re human, here…) and in fact, you have to be able to convince yourself (basically at a glance) that your code is correct. I’ve invested a lot of time in refactoring and/or hardening (both working and non-working) code to have its intentions better reflected, preferably with a reduced “mental footprint” -which cannot measured as “number of characters/lines of code” as anyone who ever tried to read an APL program will concur. This has several advantages:
- refactoring code means that you get an easy but thorough introduction to both its structure, its functionality and the relation between the two (I definitely prefer refactoring over trying to read the usual crappy/out-of-date documentation);
- anyone else who gets in touch with the same (refactored) code has it a bit easier (the possible exception being the culprit who originally wrote the crappy code);
- less, more concise code means less effort required for just about anything: bug fixing, enhancements, migration/re-writing, removing unused code, detecting and removing dead code, etc….
Almost without fail, this has provided me with a good ROI, usually in the form of bug fixes and enhancements taking very little time.
Regarding point 4: for the same reason that you can’t review your own code, I think it’s not very effective to be the only one writing unit tests for your code. You simply can’t hope to catch all the bugs which have eluded you while coding, because of “mental entrenchment”: you get entrenched in your thinking very soon, making it harder and harder to “see outside of the box”. Of course, writing your own unit tests is very good for getting up to speed with writing the code in the first place (one click, a few seconds, red means fail), as it gives you a very quick turn-around on checking whether your code is performing correctly in the most basic of senses while at the same time ensuring that your code is testable in the first place.
But, after having created and tested your code and having convinced yourself that it is as it should be, obviously bugs will remain…but they won’t be obvious to you. This is the moment where it’s useful to bring in someone else from the team and have him/her have a look at your stuff. This could be a review but it could also entail writing some extra unit tests which test for cases you hadn’t thought of yourself. This approach has three immediate advantages which a pure review seldom has:
- your code is actually live-exercised by someone who very well might be using your stuff very soon, which doesn’t only help to hunt for the nastier bugs (hardly ever discovered through review) but also validates the way your stuff interacts with other code;
- knowledge on your (and especially its API) is spread throughout the team, on a more “intimate” level than a review can hope to achieve;
- (elaborating on point 2) if the round of writing more unit tests is followed by a round of refactoring by the second person, code ownership is spread around as well;
- a review document is just another “inert” document while unit tests validate through both compilation and execution.
To elaborate the last point: a review document typically is a bunch of rather freeform text, pertaining to various aspects of the code base as well as the project as a whole (most importantly: requirements). It (usually) has no hard/navigable links into the code and it doesn’t keep itself up-to-date nor does it automatically flag itself when it gets out-of-date. Unit tests, on the other hand, at least are intimately intwined with the code they’re supposed to test and are re-executable on the click of a button -and should be executed during the continuous build, of course.
Personally, I think having a “second opinion” in the form of someone writing additional unit tests before doing a traditional code review, is a serious alternative to techniques like pair programming (which may not escape the “mental entrenchment” completely either). So, from now on my unit testing mantra will be “I’ll let you unit test mine, if you let me unit test yours!” 😉