In this section, we list some practical considerations to keep in mind when testing and debugging applications.
Unit Testing Best Practices
Now that we've seen examples of some of the more popular unit testing tools out there, here are a few tips that enable these tools to yield their maximum benefit:
Write unit tests early. Some people even say that the test cases for a class should be written before the class itself. When a component of a system is designed, the desired behavior of the component is encoded into a series of unit tests. The implementation of the component is complete when all the tests run successfully.
When writing the tests first isn't possible, extra care should be taken to prevent writing tests that simply make the existing code pass. The test cases should be derived from requirements documents and sound logic, not from an existing implementation. The author of a test case should be concerned only with the interface to an implementation, not the inner workings of that implementation.
Run tests often. This is typically something that can be run, at the very least, during an automatic nightly build. That way, any changes to the system that have broken existing functionality are detected right away.
Logging Best Practices
Having a powerful logging API at your disposal certainly makes it easier to generate and collect information about a system, but really that is only half of the solution. The real trick is to use the logging facilities to their fullest advantage. This essentially means finding the balance between logging enough relevant information to be helpful when tracking down problems, but not so much information that the system becomes overwhelmed with log files. Here are some points to consider when putting logging messages into your system:
Choose the right priority for the message. Try to come up with a convention for what constitutes an error, warning, info, debug, or other type of message, and stick to it. Messages such as emergency or critical should be used very sparingly, if at all. Treat debug messages as information that's useful for trying to locate a specific problem, but that could go away at any time if the need no longer exists. Treat info messages as key status messages that report the current state of the system.
Identify each message. With server applications, many users can be using the system at one time. In such cases, it's very helpful to extract a single user's session from the myriad log messages being generated. This can be done by placing a session ID or some application-specific user information into every message that's generated. This makes it much easier to track down the problems reported by a specific user, and to follow a single execution path through the system.
Think about what information would be useful when responding to a bug report from a user. That can be difficult because it isn't easy to know what information you'll need to know to track down some problem that you don't even know exists. Simple messages such as Entering method XXX and Leaving method XXX typically don't provide a great deal of information, but instead just clutter up the logs. Generate log messages based on conditionals and include the values of key variables.
Make logging as inexpensive as possible from a processing standpoint. This ties in with not placing logging statements all over the code as a shotgun approach to logging. Including too many log statements slows down the system and makes the code less readable. Also consider the cost of generating the log itself. Try to limit the number of string concatenations done in a single statement. When printing out variables, try to print out local String variables whenever possible. Use the StringBuffer class as an alternative when you have to do a lot of concatenation.
Debugging Best Practices
Finally, here are a few tips that will help make debugging applications go smoother:
Choose the right breakpoints. Ideally, you want the breakpoints as close to the error as possible to avoid hitting the breakpoint too often or needing to single-step through many lines of code. If an exception is thrown, work backward from there.
Consider conditional breakpoints. These breakpoints are triggered only when a given expression becomes true. This comes in handy for breaking inside loops with many iterations when you are interested in the iterations near the end. Conditional breakpoints can slow down the debugging process because they require additional overhead, so use them sparingly.
Use remote debugging to debug applications in different hardware environments. Although Java is a cross-platform language, sometimes there will be cases in which a bug will be seen in the production environment that cannot be reproduced in the development environment. With remote debugging, you can attach to that Java process on a remote machine and step through the code as if it were running locally. Obviously, this should never be done on a machine that is currently live to the public!