The title line says it all. Invest time in testing, the earlier, the better. Recently, I’ve been (re-)implementing some fairly simple class here, with a few unit tests. I tend to test at least the basic functionality with unit tests (i.e. testing good code paths), so I never commit really broken code. I was pretty sure that I did a good job on the class, after all, it was much simpler than the previous one.
Today, I started adding tests for the various corner cases and error reports. Most of my error checking is in the interfaces, so I can easily write a mockup implementation, test it and be sure all derived classes have exactly the same error checking. This one didn’t reveal anything new, except for one case, in which the class was not setting a state properly. No big issue, as this was just an error state, and the next access would fail anyway, but nevertheless, I wanted to get rid of it, and it turned out that I could simplify the code a bit. Not much, 5 LoC and a member variable less – but if you do this for many classes, the savings accumulate.
5 LoC here, 10 there, repeated over 20 classes, and at the end you might end up with a 10 KiB smaller binary. Remember, the fastest and most correct code is the one that isn’t there, so I consider this a real win. It’s quite interestingly to see how clean you can get stuff. After a few iterations, my classes are much better designed, more robust, require less code to use and have less code to maintain themselves. This sounds like the initial implementation is usually bad; but I tend to disagree – it’s only after having done it that you often see how to do it better, and at the beginning, there is no test net to safely try stuff.
However, it requires really a two-part approach:
- Early unit test start. If you add basic unit tests too late, refactoring becomes too expensive. The tests should cover the working cases.
- A code refactoring step, which should be done either after a break from this particular code or by another person. In this step, tests should be created to test error and corner cases.
I tend to write code & unit tests concurrently, and after the class is implemented, I document the most important functions. A week later or so, I go back and add unit tests for failure cases, trying to break the class. During this time, I almost always find parts to refactor, either because I see that I repeat code while writing the unit tests, or because I hit bugs. Funnily enough, it really takes a break before I get a feeling for where a class or function may break. This is probably something which can be done better in pair programming, but unfortunately, I didn’t have a chance to try that yet.
So much for today, happy coding!