I had an “ahah” moment this afternoon while wrapping tests around some legacy Java code that lacked any. To create an instance of the class, I had to construct object graphs of “stunt doubles” to pass to the constructor. The doubles had to have just enough behavior to pass muster with private methods in the parent class that were invoked form the parent constructor. Getting an initial spike working took a lot of “let’s just pass null and see how far we get,” followed by hand-rolling mock objects.
When I could finally create an instance, I began attacking the class’s single instance method. By eyeball, its cyclometric complexity was about 40. Lots of branches to cover, and it was already late in the day.
That’s when I had the ahah!
By setting breakpoints on every statement in the method, running the tests in the debugger, and clearing each breakpoint as it was hit, I could get a quick checklist for what lines of code where still untested. This was a lot faster than generating a coverage report after every successful test run to show me which lines were still untested.
I’m sure this is an old trick, but it was new to me. (Possibly because, outside of Java, I rarely spent time in debuggers.)
And yes, I know that it’s really not that simple. This technique doesn’t account for things like zero-trip for loops, which often represent hidden branches. The hidden branches I had to note separately as I went. And yes, you really only need breakpoints on the first statement of each basic block, but that takes thinking, and the time that takes is easily canceled out by how fast it is to just put breakpoints on every line that’ll take one. Plus, stopping at each line once gives you a chance to notice little things, which can pay off when you’re working with legacy code.