The class as a testable unit
If an organization is using the object-oriented paradigm to develop software systems it will need to select the component to be considered for unit test. As described in Section 6.1, the choices consist of either the individual method as a unit or the class as a whole. Each of these choices requires special consideration on the part of the testers when designing and running the unit tests, and when retesting needs to be done. For example, in the case of the method as the selected unit to test, it may call other methods within its own class to support its functionality. Additional code, in the form of a test harness, must be built to represent the called methods within the class. Building such a test harness for each individual method often requires developing code equivalent to that already existing in the class itself (all of its other methods). This is costly; however, the tester needs to consider that testing each individual method in this way helps to ensure that all statements/branches have been executed at least once, and that the basic functionality of the method is correct. This is especially important for mission or safety critical methods.
In spite of the potential advantages of testing each method individually, many developers/testers consider the class to be the component of choice for unit testing. The process of testing classes as units is sometimes called component test . A class encapsulates multiple interacting methods operating on common data, so what we are testing is the intraclass interaction of the methods. When testing on the class level we are able detect not only traditional types of defects, for example, those due to control or data flow errors, but also defects due to the nature of objectoriented systems, for example, defects due to encapsulation, inheritance, and polymorphism errors. We begin to also look for what Chen calls object management faults, for example, those associated with the instantiation, storage, and retrieval of objects .
This brief discussion points out some of the basic trade-offs in selecting the component to be considered for a unit test in object-oriented systems. If the class is the selected component, testers may need to address special issues related to the testing and retesting of these components. Some of these issues are raised in the paragraphs that follow.
Issue 1: Adequately Testing Classes
The potentially high costs for testing each individual method in a class have been described. These high costs will be particularly apparent when there are many methods in a class; the numbers can reach as high as 20 to 30. If the class is selected as the unit to test, it is possible to reduce these costs since in many cases the methods in a single class serve as drivers and stubs for one another. This has the effect of lowering the complexity of the test harness that needs to be developed. However, in some cases driver classes that represent outside classes using the methods of the class under test will have to be developed. In addition, if it is decided that the class is the smallest component to test, testers must decide if they are able to adequately cover all necessary features of each method in class testing. Some researchers believe that coverage objectives and test data need to be developed for each of the methods, for example, the create, pop, push, empty, full, and show_top methods associated with the stack class shown in Figure 6.3. Other researchers believe that a class can be adequately tested as a whole by observation of method interactions using
a sequence of calls to the member functions with appropriate parameters.
Again, referring to the stack class shown in Figure 6.3, the methods push, pop, full, empty, and show_top will either read or modify the state of the stack. When testers unit (or component) test this class what they will need to focus on is the operation of each of the methods in the class and the interactions between them. Testers will want to determine, for example, if push places an item in the correct position at the top of the stack. However, a call to the method full may have to be made first to determine if the stack is already full. Testers will also want to determine if push and pop work together properly so that the stack pointer is in the correct position after a sequence of calls to these methods. To properly test this class, a sequence of calls to the methods needs to be specified as part of component test design. For example, a test sequence for a stack that can hold
three items might be:
create(s,3), empty(s), push(s,item-1), push(s,item-2), push(s,item-3), full(s), show_top(s), pop(s,item), pop(s,item), pop(s,item), empty(s), .
The reader will note that many different sequences and combination of calls are possible even for this simple class. Exhaustively testing every possible sequence is usually not practical. The tester must select those sequences she believes will reveal the most defects in the class. Finally, a tester might use a combination of approaches, testing some of the critical methods on an individual basis as units, and then testing the class as a whole.
Issue 2: Observation of Object States and State Changes
Methods may not return a specific value to a caller. They may instead change the state of an object. The state of an object is represented by a specific set of values for its attributes or state variables. State-based testing as described in Chapter 4 is very useful for testing objects.
Methods will often modify the state of an object, and the tester must ensure that each state transition is proper. The test designer can prepare a state table (using state diagrams developed for the requirements specification) that specifies states the object can assume, and then in the table indicate sequence of messages and parameters that will cause the object to enter each state. When the tests are run the tester can enter results in this same type of table. For example, the first call to the method push in the stack class of Figure 6.3, changes the state of the stack so that empty is no longer true. It also changes the value of the stack pointer variable, top. To determine if the method push is working properly the value of the variable top must be visible both before and after the invocation of this method. In this case the method show_top within the class may be called to perform this task. The methods full and empty also probe the state of the stack. A sample augmented sequence of calls to check the value of top and the full/empty state of the three-item
empty(s), push(s,item-1), show _top(s), push(s,item-2), show_top(s), push(s,item-3), full(s), show_top(s), pop(s,item), show_top(s), pop(s,item), show_top(s), empty(s), . . .
Issue 3: The Retesting of Classes—I
One of the most beneficial features of object-oriented development is encapsulation. This is a technique that can be used to hide information. A program unit, in this case a class, can be built with a well-defined public interface that proclaims its services (available methods) to client classes. The implementation of the services is private. Clients who use the services are unaware of implementation details. As long as the interface is unchanged, making changes to the implementation should not affect the client classes. A tester of object-oriented code would therefore conclude that only the class with implementation changes to its methods needs to be retested. Client classes using unchanged interfaces need not be retested. In an object-oriented system, if a developer changes a class implementation that class needs to be retested as well as all the classes that depend on it. If a superclass, for example, is changed, then it is necessary to retest all of its subclasses. In addition, when a new subclass is added (or modified), we must also retest the methods inherited from each of its ancestor superclasses. The new (or changed) subclass introduces an unexpected form of dependency because there now exists a new context for the inherited components.
Issue 4: The Retesting of Classes—II
Classes are usually a part of a class hierarchy where there are existing inheritance relationships. Subclasses inherit methods from their superclasses. Very often a tester may assume that once a method in a superclass has been tested, it does not need retested in a subclass that inherits it. However, in some cases the method is used in a different context by the subclass and will need to be retested. In addition, there may be an overriding of methods where a subclass may replace an inherited method with a locally defined method. Not only will the new locally defined method have to be retested, but designing a new set of test cases may be necessary. This is because the two methods (inherited and new) may be structurally different. The antiextentionality axiom as discussed in Chapter 5 expresses this need .
The following is an example of such as case using the shape class in Figure 6.4. Suppose the shape superclass has a subclass, triangle, and triangle has a subclass, equilateral triangle. Also suppose that the method display in shape needs to call the method color for its operation. Equilateral triangle could have a local definition for the method display. That method could in turn use a local definition for color which has been defined in triangle. This local definition of the color method in triangle has been tested to work with the inherited display method in shape, but not with the locally defined display in equilateral triangle. This is a new context that must be retested. A set of new test cases should be developed. The tester must carefully examine all the relationships between members of a class to detect such occurrences.