A Practical Guide to Testing Object-Oriented Software

An exception provides an alternative route for returning from a method that does not necessarily move to the next statement after the method is invoked. Exceptions are powerful in two respects:

  • The exceptional return value is an object and can be arbitrarily complex.

  • The points at which an exception is thrown varies based on the depth of the aggregation hierarchy.

Most interface designers use exceptions to handle error conditions that can arise during processing. Exceptions provide an alternative to return codes and in some situations can reduce the amount of code needed to process return codes. However, exceptions are also useful for processing exceptional conditions that arise during processing that are not really associated with errors. Our design for Brickles uses exceptions to terminate play of the game when either the puck supply is exhausted or the last brick is broken. Figure 6.24 shows how exceptions and return codes can be used in C++ to handle a problem reading from an input stream.

Figure 6.24. Code structure for return code and exception methods of error handling

While the prototype for readInt(int) documents that only exceptions of the class ReadError (and its derived classes) will be thrown, C++ does not require a function (or member function) to list the types of exceptions it can throw. This presents a problem for testing (and probably for developing as well!) in that postconditions may not be tested completely. There is always a possibility that an unexpected perhaps system-level exception could be thrown within the context of a function's execution. Consequently, it is a good practice in C++ development to use exceptions to fully specify any interface that uses exceptions.

"Testing exceptions" provides two different perspectives. First, at the class-testing level the focus is on whether each of the methods in that class does in fact throw the exceptions that it claims to in the appropriate circumstance. This will be handled as a normal part of class testing since each potential throw should be a postcondition clause. The PuckSupply class would have tests that determine that the OutOfPucks exception is thrown when the puck supply has been exhausted. The coverage criteria requires that a class throws every exception contained in the specifications of the methods. There would be at least one test case per exception.

The test driver establishes the conditions in the object under test that will result in an exceptional event. The driver provides a try block inside which a stimulus invokes the method that throws the specific exception. The exception is caught by the test driver and verifies that it was the correct exception. Since exceptions are objects and belong to classes, the catch statements can use the typing system to verify that the exception is of the correct type.

Second, during integration, interaction testing will determine whether these exceptions that are being thrown at the correct time are being caught at the correct place. This is a test of the interaction between the originating object that initiates the sequence of method invocations that result and catch the exception, and the throwing object that reaches an exceptional state and throws the exception. For example, in the Brickles game, when the OutOfPucks exception is thrown, is it caught? Is it caught in the correct place? The originating object is several levels of aggregation removed from the PuckSupply object that actually throws the exception.

The test driver, in this case, instantiates the originating object. The originating object is responsible for creating those levels below it in the aggregation hierarchy. The test driver stimulates the originating object to create all of the levels and to place the originating object into a state in which the lower-level object will throw the exception. The coverage criteria for this level of testing is to be certain that every exception thrown is caught in the correct location.

Both of these points of view can be tested very early in development. During the guided inspection of the system-level design model, every user-defined exception that is instantiated should be traced to an object that will catch the exception using the sequence diagram for the scenario that throws the exception.

Testing Interactions at the System Level

At some point, components become so complex that it is easier to test them in the context of the application itself instead of in the environment provided by a test driver. Some parts of the system might not be modeled by a single class. For example, the user interface provided by most application programs is not a single instance of some class, but a community of objects that supports input and output. The interactions that can be tested at the system level are only those that can also be verified at the system level. That means that we can see the direct results of the tests, and it also means that there must be a direct relationship between the user interface and the ability to view test results.

Категории