A Practical Guide to Testing Object-Oriented Software
We usually expect the root class of an inheritance hierarchy and even some of its direct descendents to be abstract. In this section, we discuss possible ways of testing abstract classes, such as Sprite and MovableSprite. Execution-based testing of classes requires that an instance of the class be constructed. Most object-oriented programming languages, including C++ and Java, support syntax for identifying abstract classes. Language semantics generally preclude instances of abstract classes from being created. This presents a problem for testing because we cannot create the instances we need. We have identified some different approaches to testing abstract classes. Each has strengths and drawbacks. We present these approaches in the context of testing the Sprite abstract class. One approach to testing an abstract class, such as Sprite, is shown in Figure 7.9. Under this approach, a concrete subclass of Sprite is defined solely for the purpose of testing. In the figure, we name this class ConcreteSprite. The implementation of ConcreteSprite defines a stub for each abstract operation of Sprite. If one or more of the methods in Sprite is a template method using one of Sprite's abstract operations, then that abstract method must be stubbed appropriately so that it effectively appears to meet the postconditions for the operation it stubs. In some instances, this is not difficult to accomplish. For some complex operations, writing a satisfactory stub can require substantial effort. Once a concrete subclass has been implemented, the objectUnderTest() factory method of the Tester class for example, SpriteTester creates an instance of the concrete subclass. Figure 7.9. One approach for the execution-based testing of an abstract classOne disadvantage to this approach is that the implementation of abstract methods cannot be propagated easily to abstract subclasses without using multiple (repeated) inheritance. Consider, for example, what is now necessary for testing the abstract class MovableSprite, which is a subclass of the abstract class Sprite illustrated in Figure 7.10. Ideally, the ConcreteMovableSprite class could reuse the stubs implemented in ConcreteSprite. However, this reuse is not immediate unless ConcreteMovableSprite inherits from both MovableSprite and ConcreteSprite. While multiple inheritance is available in C++, it is not in most object-oriented programming languages nor is its use for this purpose encouraged. Figure 7.10. Another approach for the execution-based testing of an abstract classA second approach to testing an abstract class is to test it as part of testing the first concrete descendent. In the context of testing Sprite, this would be done in testing, say, Puck. This approach eliminates the need to develop extra classes for testing purposes at a cost of increased complexity in testing this concrete class. In writing the Tester class for a concrete class such as Puck, we need to take care to implement a Tester class for each ancestor, thereby providing each with the appropriate and correct test case and test script methods. This is straightforward to do in practice. Careful review of the code in the Tester classes for the abstract classes can reduce the effort needed to get the concrete subclass Tester class implemented correctly. If the concrete subclass passes all its test cases, then the assumption is that the ancestor classes pass their test cases. This assumption is not always valid. For example, a concrete subclass might override a method defined in one of the abstract classes. In that case, another concrete subclass that does no such override must be used to test that method. Neither of these approaches is completely satisfactory. We have investigated a third approach based on the direct implementation of a concrete version of an abstract class for testing purposes. In other words, we have tried to find a way to write source code for a class so that it can easily be compiled as an abstract or a concrete class. However, neither an approach based on editor inheritance nor one based on conditional compilation[7] has produced good results because the resulting code is complex and hard to read, thus it's susceptible to error. [7] Editor inheritance refers to the cloning of code by copying existing source code and then editing the copy to add, remove, or change its function. In this case, we copy the code for the abstract class to a separate file, then implement any abstract operations to make the class concrete. Of course, the main drawback of editor inheritance is that any change to the original source code is not propagated automatically to the cloned source code. To work around this drawback, we can use conditional compilation based on, for example, C++ preprocessor directives #if defined(TEST) and #elif and #endif, to put the code for abstract and concrete versions of the same class in the same source file. Using conditional compilation makes code very difficult to read and maintain. A good alternative is to test an abstract class using guided inspection instead of execution-based testing. Reviews are acceptable because a typical abstract class provides little or no implementation for the abstract operations. In our experience, public interfaces for abstract classes tend to stabilize fairly quickly. The concrete operations are primarily inspectors or simple modifiers that can easily be tested by inspection. Constructors and destructors are more complicated to test by inspection only for example, the constructor for the Sprite class involves finding a bitmap image associated with a resource ID. We still prefer to do execution-based testing of concrete classes because it supports easier regression testing. PACT offers advantages for testing families of classes, so we still want to develop Tester classes for abstract classes. In our practice, we prefer the second approach we discussed that is, testing abstract classes with the first concrete subclass to be tested. This approach is straightforward and requires relatively little additional coding effort for implementing testers. We still have the advantage of easily performing regression testing. |