There once was an Old West town called Two Dudes. There were only two buildings in Two Dudes — a jail and a saloon. The town’s eponymous pair of inhabitants included the town sheriff, who also kept bar at the saloon by day (or did he do that at night? One never can be sure with these old yarns) and the saloon’s sole patron, a man by the name of Bad Ed. Bad Ed was a notorious thief, and once he’d downed a few drinks he’d usually round out the night with a crime spree. Fortunately for the town, the sheriff knew exactly where to find Bad Ed, and always had him behind bars by the next morning.
(Bad Ed never spent more than a few hours in jail, of course, because how was the sheriff going to make a living if the saloon didn’t have any customers? Er, customer.)
Much like detecting crime in Two Dudes, finding bugs in a two-line codebase is easy. Debugging only becomes hard when the guilty line of code is hiding among a thousand innocent ones, and you don’t know where it’s hiding when you start investigating. You have to investigate (and reject) many more incorrect hypotheses, before you find the correct one.
This might be why programmers, on average, spend about 50% of their time debugging.1 That’s a lot! My own experience indicates that this percentage can probably be reduced significantly, and that programming becomes a lot more fun when you don’t have to debug as much.
If only all our codebases could be like Two Dudes, we’d never need a debugger. We’d never need to write another print or console.log. Of course, life can’t be that simple.
Or can it?
If you had some way of spotting a bug immediately after writing it, you wouldn’t need a debugger. You’d know which line was guilty: it’s the line of code you just wrote. So you’d just hit ctrl+Z, undo your last change, and the bug would be gone.
It’s actually possible to work like this, as long as you:
Maintain a complete suite of tests (or other automated verification checks) at all times.
Derive your tests from your intentions for what the code should do, not from what it happens to do currently. Then, make the code match the tests. Not the other way around.2
Work in very small steps.3 Keep the intervals in which your code will not compile or run very short, ideally on the order of a few seconds. Here is a tiny technique that helps with this.
Run your tests continuously — every time your code reaches a compilable state.
Following these “zero-bug rules” ensures that you will notice most bugs as soon as you introduce them, and can revert them immediately. As a bonus, you’ll understand your code so well that any bugs that do slip through to production will be easy to diagnose. Your reaction to a bug report will be “d’oh! I should have thought of that!” instead of “that shouldn’t be possible!”
For many software developers, the big stumbling block is the “run your tests continuously” rule. You can’t follow this rule if your test suite takes minutes to run. You can’t even really do it if it takes 10 seconds to run. You need to aim for the Doherty threshold: feedback in 400 milliseconds or less. In future posts, I’ll talk about how to design your code so it’s possible for tests to run this quickly. For now, I’ll just assert that it’s possible.
One development process you can use to implement the zero-bug rules is test-driven development. I happen to like TDD a lot, but maybe you don’t. Fortunately for you, there are probably other possible implementations of the zero-bug rules. Maybe you’ll invent the next great one.
Debugging: the biggest waste by Daniel Moka
This criterion could probably be included as part of the definition of a “complete” suite of tests. If a test doesn’t describe intentional behavior, it can’t contribute meaningful coverage (because when it fails, you don’t know if the code is wrong, or the test is wrong).
Sometimes, the purpose of tests is simply to preserve the current behavior of the software. This is true, e.g. for “characterization tests” written to pin down legacy code for refactoring. In this case, you should derive tests from the code, because the goal is “bug-for-bug compatibility.” However, you should not be characterization-testing newly written code; you should be doing test-driven development. If newly written code has bugs, you don’t want to bake them in with tests.
GeePaw Hill defines a “step” as “a purposeful gap of unreadiness between two points of readiness on some system’s timeline, a before point and an after point.”

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.