In Diamond in the Rough I talked some about the similarity I found between David Gries’ work on proving programs correct in The Science of Programming and actually doing TDD. I’ve since gone back to re-read Science. Honestly it hasn’t gotten any easier to read since I last read it so long ago. It’s still a worthwhile, though, if you can find a copy. It’s a nice corrective to an issue I’ve seen with a lot of developers over the years: they just don’t have the skills or maybe the inclination to reason about their code.
This seems most evident in an over-reliance on debuggers. For myself, I only use a debugger when I have a specific question to answer, and I can’t answer it immediately from the code. “What’s the run-time type of this visitor given these circumstances?” of “Which of the five different objects on the silly call chain was null when it was used?” (that’s a little lazy, since I could refactor the code to make the answer clear without the debugger, but I’m generally trying to find the unit test to write to expose the problem before I fix it, and I don’t want to refactor until I have a green bar that includes the fix to the actual problem). Those are the types of questions I might turn to the debugger to help answer. Particularly when the cost of setting up a test to get that type of feedback is unusually high compared to using a debugger. This is common when my code is interacting with third-party code in a server container. Trying to set up some kind of integration or functional test to get the very specific information I want can be a horrible rabbit hole. (Although it may still be worth setting up the functional test to prove a problem is actually solved.)
So I do recognize times when one can get good feedback more effectively from a debugger than immediately trying to write a unit test or just reasoning about the code one is looking at, but it worries me when the debugger is the first tool a developer uses when something doesn’t act as they expect. Even when I try asking some developers the questions that I ask myself when trying to understand the problem, the first response is to fire-up the debugger, even if it’s patently not a question that needs a debugger to answer (“What kind of type is that method returning?” when the actual method declaration is no more than twenty lines down and declares that it’s returning a concrete type). And, most egregiously, I’ve often seen people debugging their unit tests.
That’s really worrying since the unit tests are meant to help us understand our code. It concerns me when I see someone’s first response to a unit test failure (even a test they’re just writing) is to run it through the debugger. Which is not to say that I never do so, but for me it’s always a case of “I know this test failed because this variable wasn’t what I expected it to be. What was it?” Again, for me the debugger is a means for getting a specific piece of information rather than something I try to use to help me understand the code. And that seems a much more efficient and effective way to get the code to do what I want it to do.
The more general case for turning to the debugger seems to be when one doesn’t understand the code. It’s a little more understandable when trying to understand someone else’s code. Even then, though, I’m not convinced that the debugger is the best option for trying to really understand what the code is doing. In a case like this, I’d comment out the code and write unit tests to make me uncomment one small part at a time. This forces me to really understand what the code is doing because setting up the tests correctly helps me look at the various conditions that affect the code. Gries’ techniques come into play here, too. It’s unconscious for me now, but the ability to reason formally about the code helps lead me into each new test that will make me uncomment the smallest possible bit of code.
So, how do we learn to reason about our code rather than turning to the debugger as an oracle that will hopefully explain it? Part of it may be the skills one learns in Gries’ Science, even if they’re not formally applied. The stronger influence, however, may be the way I learned to practice TDD. I do genuinely test-drive my code and when I first learned TDD it was drilled into me to write my failing test and state why the test would fail. After not really writing one’s tests before the code, not asking that simple question seems the biggest failure in how I’ve seen TDD practiced. That might be the better way to learn to really reason about what one’s code is doing. While I still respect and appreciate the techniques that Gries described in Science, it’s probably both easier and more efficient to learn the discipline of really writing tests before the code, asking the why the test should fail and thinking about it when it fails differently than one expects.