We discuss the crucial interdependence between effective debugging and effective development, highlight extreme programming development methods, and peek at the potential future of test-oriented languages.
Debugging as Scientific Experiment
To debug software is to perform a type of scientific experiment and just as scientific experiments are best done in a laboratory environment, with controlled procedures and equipment, debugging software is best done as part of an integrated method of software development. Indeed, an appropriate method of development can easily help prevent many bugs from ever occurring in the first place.
Even when bugs are found, the development method can make a big difference in how quickly a bug can be diagnosed. As you might have guessed, I strongly advocate extreme programming as a particularly effective development method when you want to diagnose bugs quickly. And who doesn't want to find and dispose of bugs quickly?
Let's talk specifically about how extreme programming facilitates the debugging process. Although a thorough introduction to extreme programming is beyond the scope of this text, I have found the following aspects of XP to be quite useful in debugging:
- Software is specified, integrated, and released incrementally.
- Design is kept as simple as possible.
- Programming is done in pairs.
- An on-site customer is always available.
- Code is owned by all developers.
- Tests exist for "anything that could possibly break."
How do each of these practices help in debugging software? Let's see.
Software is specified, integrated, and released incrementally
In extreme programming, each aspect of the functionality is specified just before it is implemented. A pair of programmers will then work on implementing that aspect for a couple of weeks or less (remember, it's just a small bit of functionality), and then release the new version. Along the way, they will integrate the work they've done thus far into the system, ensuring that all the tests they wrote and the tests everyone else wrote still pass.
In this way, any bugs that are introduced have a very good chance of being caught right away by the unit tests. Additionally, since implementation is incremental, the programmers will be wholly focused at each integration on making sure that one small piece of new functionality works.
Why do it? Catch bugs as they're introduced; focus is on a single, small bit of functionality.
Design is kept as simple as possible
Complexity is the enemy of robustness. If there is one aspect of software construction that laymen find most puzzling, it's that good programmers treat complexity like an unwanted weed, rooting it out to the extent possible. Why? Because programmers know that software systems are often used much longer than anyone expects, in contexts never anticipated, growing larger and larger all the while. Unless complexity is actively resisted, it'll quickly overtake a project.
Extreme programming continually strives to keep the system as simple as possible. Collective code ownership and constant refactoring (facilitated by the heavy use of unit tests) help to keep the code simple. Of course, that helps to prevent bugs from being written in the first place. But it also helps debugging; the simpler a program is, the fewer possibilities have to be considered as potential causes of a bug.
Why do it? Simpler code means less potential causes for a bug.
Complexity is the enemy of robustness.
Programming is done in pairs
In extreme programming, all programming is done in pairs. Partners take turns driving at the keyboard. The partner who isn't driving is navigating-that is, inspecting the code as it's written, considering the big picture, and determining if there's a better way to do things. In essence, the navigator performs a continuous code review.
The motivation behind pair programming is that the act of coding involves simultaneously considering high-level aspects of a program, such as encapsulation and abstraction, and thinking about low-level aspects, such as syntax, argument order, etc. By using two people, we can distribute this cognitive burden more effectively.
Believe it or not, two programmers working together can be just as efficient (or even more efficient) than the same two programmers would be if working separately. At first, that sounds absurd. Since they're both working on just one task, as opposed to the two they'd work on separately, we would expect that productivity would be cut in half. But, as amazing as it sounds, time and time again pair programming increases overall productivity.
No doubt there are many reasons why pair programming is so efficient, but there's one that anyone who has ever debugged a program can appreciate. Consider the most common form of bug introduced in a program: a typo or syntax error. Even when these bugs are caught by static type checking, it takes time to go through the tedious cycle of compile, fix, compile, etc. The navigator will prevent many such typos from making it to the compile stage. This alone can save a surprising amount of time. But the real time savings occurs when the navigator discovers a typo that actually would have made it past static checking. In those cases, tracking down the bug can involve a substantial amount of cognitive effort and time (often several hours). If the navigator finds just two such bugs in a four-hour pairing session, the pairing will have paid for itself. After that, any further time savings is in the black.
Why do it? Divides thinking about highand low-level issues; increases person-hour productivity; provides backup on error introduction.
An on-site customer is always available
In contexts in which requirements are ambiguous and changing frequently, having a live customer available to ask questions is extremely helpful. It can often prevent developers from wasting precious time guessing how the customers would like some functionality to be implemented, only to discover that they were completely wrong.
In the context of effective debugging, the important aspect here is that the programmers are getting continuous feedback from someone who is actively using the product, and the more people using it, the better. Inevitably, these users will discover bugs that escaped even the most thorough test suite. As always, the sooner a bug is discovered, the easier it is to fix.
Why do it? Provides active user/client feedback.
Code is owned by all developers
When all programmers have the authority to refactor the code written by any programmer on the team, they tend to become more familiar with that code. Extreme programming involves not just collective ownership of code, but uniform coding standards and constant refactoring. When programmers are given the authority to review and improve all code in the project, they naturally become more familiar with the code they didn't write. This facilitates effective debugging in several respects.
Most obviously, they will be much better able to debug each other's code, since they'll understand it better. But they will also be less likely to blame a bug on code they didn't write. Humans tend to be suspicious of things they're unfamiliar with. That's a great survival trait when deciding which wild plants to eat, but it's deadly when debugging software. Programmers place blame all too often on anything but their own code. Collective code ownership ameliorates this phenomenon.
Why do it? Increases familiarity of all code for all programmers; lessens time spent blaming "foreign" code.
Tests exist for "anything that could possibly break"
In extreme programming, programmers write tests for functionality even before they start implementing that functionality, and they continue writing tests as they finish implementing it. Every time a programming pair integrates their code, all tests are run to ensure that the new code doesn't break anything. Unit tests form part of the specification of a program and they help prevent the members of a programming team from breaking each other's code. And since unit tests are run every time new code is integrated, they help to catch many bugs as soon as they're introduced. Even in cases where a bug isn't detected until long after it was introduced, the unit tests help programmers to eliminate many possible causes of the erroneous behavior and diagnose the cause of the problem quickly.
What does it mean to say "test anything that could possibly break"? Any method with a non-trivial body should be tested. Getters and setters on the other hand may not require testing since they are trivial and utterly fail to work if they are mistyped.
Why do it? It ensures that new functionality is consistent with existing functionality.
Taken together, these concepts make up a very powerful environment for debugging software. In fact, I've found that bugs resulting from mere typos can be virtually eliminated with extreme programming. An effective development method is the single most important quality of an effective debugging strategy.
Incorporate Debug Tests into Unit Test Suites
Because one small test will often lead us to discover the true source of a bug, it follows that effective debugging should involve lots of testing. There's simply no way to build a reliable system without thoroughly testing it.
Although many programmers may be unfamiliar with the XP style of coding (in which writing the tests is interleaved with writing the code), there is one form of testing that everyone does while coding: debugging. Some programmers write these tests using print statements, others write them to work with a standalone debugger, others with the debugger in their IDE of choice.
Quite often, you will see programmers discard these tests once they've gotten the program to run correctly. This is a waste of very good tests. Why not incorporate them into the unit test suite over a program? After all, if one of them were to exhibit an unexpected result, we'd want to know about it.
Incorporating debugging tests into the unit tests can be done with relatively little effort, but there are some adaptations that have to be made. Frequently, we'll write tests for debugging to display information about the internal state of the program, whereas we'll write unit tests to signal only if they fail. That's because debugging and unit testing serve different purposes.
When debugging, we are trying to form a more accurate model of the program's behavior, so as to diagnose a bug. But when testing, all we want to know is whether the code passed the tests. If instead we wrote the unit tests to print out a result and then manually checked that it was what we expected, we'd waste a lot of time (or, more likely, we would seldom run the unit tests). So, unit tests tend to be written such that the result is solely one of "pass" or "fail".
Debugging tests can still be used as unit tests with only slight modification. Consider that when the program is working correctly, the result of running a debugging test will match some expected result. It is straightforward to modify such a test so that, instead of simply printing out this result, it compares the result to what's expected. It can then be incorporated into a unit test suite quite easily.
Just as debugging helps in developing a large suite of unit tests over a program, unit tests can help significantly when debugging. When diagnosing a bug, if you can first run a suite of unit tests and verify that they pass, you can rule out a huge number of potential explanations for a bug. In this way, unit tests allow you to leverage your cognitive energy when modeling program behavior. This is yet another way that debugging is like performing a scientific experiment.
When a physicist forms an explanation of an experimental result, he automatically rules out all sorts of explanations that would defy a set of accepted principles about the way the world works. For example, he assumes that the results of his experiment will not change depending on the current weather conditions on Jupiter (well, unless he is performing an experiment on Jupiter), or depending on what he plans to eat for dinner. Unit tests enforce accepted principles of program behavior.
The Future: Test-Oriented Languages
Just as many design patterns add multiple classes to a program (such as visitors, decorators, and such) that serve no purpose other than to add extensibility to the program, it is acceptable to develop new patterns to facilitate testing.
Indeed, many of the features of object-oriented languages are included to facilitate extensibility; why shouldn't future versions of such languages (or entirely new languages) include features to facilitate testing?
In the case of the Java language, this is already beginning to happen. Future versions are scheduled to include much more powerful type systems, assertions, and the like. Just as object-oriented languages have increased the degree to which we can reuse and extend existing code, future, test-oriented designs and features will help to increase the robustness of our code, both old and new.
Related Online Articles:
- Bug Patterns in Java - The Fictitious Implementation
- Bug Patterns in Java - Platform-Dependent Patterns
- Bug Pattern in Java - Saboteur Data
- Bug Pattern in Java - Building Cost-Effective Specifications with Stories
- Bug Patterns in Java - The Orphaned Thread
- Bug Pattern in Java - Debugging & the Testing Process
- Bug Patterns in Java - The Run-On Initialization
- Bug Pattern in Java - Other Obstacles to Factoring Out Code.
- Bug Pattern in Java - Saboteur Data
- Design Patterns for Debugging - Maximizing Static Type Checking
No comment yet. Be the first to post a comment.