A Practical Guide to Testing Object-Oriented Software

Testing Collection Classes

Collection classes are tested using techniques for primitive classes (see Chapter 5). A test driver will create instances[3] that are passed as parameters in messages to a collection being tested. Test cases center primarily around ensuring that those instances are correctly incorporated into and removed from the collection. Some test cases address any limitations placed on the capacity of the collection. The precise class of each of the objects used in testing a collection class is insignificant in determining the correct operation of the collection class since there is no interaction between a collection instance and the objects in a collection. If forty or fifty items might be added to a collection during actual use, then generate test cases that add at least fifty items. If no estimate on a typical upper bound is possible, then test with a very large number of objects in the collection.

[3] The factory methods for creating an object under test (see OUT Factory Methods on page 189.) are useful in creating instances used in interaction tests.

The behavior of a collection object under circumstances in which it cannot allocate memory to add the new item to itself should be tested. Structures such as growable arrays often allocate the space for several items at one time. Tools are available to help the tester to limit the amount of memory available during the execution of test cases that check the allocation of a larger-than-available block of memory. An object under test should return the appropriate exception to the requestor of the action. We will address this issue in the Testing Exceptions section on page 245.

If the defensive design approach has been used, negative tests should be a part of the test suite. Some collections have a finite capacity specified, and all collections have some practical limit such as available memory that should be tested with tests that exceed the specified limits. If a collection class uses an array as its storage, then the usual test cases for filling the array and then attempting to add one more item should be included. The appropriate exception should be generated by the object under test and caught by the object that sent the message. If a contract approach has been used, such tests are meaningless.

An important aspect of testing collection classes and testing collaborator classes as well is testing sequences of operations that is, the way modifier operations on a single object interact with one another. The techniques associated with state-based testing (see Chapter 5) can be applied to testing this aspect of collections.

Testing Collaborator Classes

The complexity of testing a collaborating class is greater than that of testing a collection class or a primitive class. Consider the class BrickPile in the Brickles application. A brick pile is an aggregation of bricks arranged in a rectangular fashion. The BrickPile class is similar to a collection class, but the BrickPile sends semantically meaningful messages to the individual Bricks for example, to determine a brick's position on a playfield or to break a brick. It is impossible to test BrickPile without using instances of Brick. It will be hard to identify faults in BrickPile if certain types of faults exist in Brick. A brick pile is responsible for detecting collisions between the bricks it contains and movable sprites (namely pucks), but it is not responsible for processing those collisions. It is also responsible for recording hints associated with breaking bricks so that the screen can be updated efficiently by the Brickles view object.[4]

[4] A hint is directed at the system components that draw the playfield, thus it provides information about the damaged parts of the playfield.

In order to test class BrickPile, we must use one or more instances of each of these classes. In fact, an instance of BrickPile cannot be constructed without an instance of a PlayField, a CPoint, and a Hint because these must be passed as parameters to a constructor (see Figure 6.3). Of course, it will need to use instances of Brick to create a brick pile.

Hint, CPoint, and Brick are all primitive classes and can be tested using the techniques presented in Chapter 5. The CPoint class used in Brickles is one of the Microsoft Foundation Classes (MFC) and is consequently "trusted," meaning that we won't test it at all. PlayField and BrickPile are not primitive and must be tested in the context of their interactions with the code in other classes using techniques discussed in this chapter.

Friend Functions

Ordinarily, a class interface comprises all the operations and, heaven forbid, data declared public. However, when using a language such as C++ that also supports friend functions, which are nonmember functions that can access the hidden parts of a class, we include any such functions in the interface. For example, many classes have defined an associated insertion operator (operator<<) that allows an instance's state to be streamed, that is, written outside the current program to a file or some other sequential structure. Treat such functions as operations in the public interface for a class. This is also the perspective taken by a programmer using the class.

The Interaction between Testing and Design Approach

The differences between contract and defense design techniques (see Implications of Defensive and Contract Designs for Testing, on page 221) extend to testing. Contract design places more responsibility on the human designer than on error-checking code. This reduces the amount of class-level testing since there are fewer paths due to a smaller amount of error-checking code. However, at the interaction level, there is more testing required for contract-designed code in order to be certain that the human designer has complied with the client side of the contract using precondition constraints.

A focus of interaction testing for contract design is whether the preconditions of methods in a receiving object are being met by the sending object. It is not legitimate to build test cases that violate these preconditions. It is usually legitimate to set the receiving object into a certain state and then begin a scenario with the sending object, which requires the receiving object to be in another state. The intention is to determine that the sending object checks the preconditions of the receiving object before sending the message inappropriately. The test should also check whether the sending object aborts correctly, probably by throwing an appropriate exception.

For example, consider the following specification for the broken() method from BrickPile in which a brick pile interacts with Brick objects (see Figure 6.4). If a design by contract approach is being used, a test case in which brick_p is 0 (null) is meaningless. A test case in which brick_p points to a specific brick instance should be used and the test case should clearly verify that the postcondition has been satisfied.

Figure 6.4. An OCL specification for the BrickPile class

In testing BrickPile, we need test cases that exercise interactions with a PlayField. In this context, there should be a test case in which PlayField is told to "break" a specific brick that is already broken. The test case is checking to be certain that PlayField is checking and will not send the broken() message to a BrickPile instance in violation of the precondition.

Категории