Object Oriented Testing
Object Oriented Testing
B.
69
70
POINT CLIENT
Client Uses
SHAPE
Data M -origin: 1 M
-point: POINT +setPoint(in X:float,in Y:float): void +getX(): float +getY(): float
POINT
+Area(): float
Polygon
-points: [] POINT +setPoints(in pointVector:[] POINT): void +getPoints(): [] POINT +Area(): float
CURVE
-points: [] POINTS +setPoints(points:[] POINTS): void +getPoints(): [] POINTS +Area()
RECTANGLE
+setTopLeft(in p:POINT): void +setBottomRight(in p:POINT): void +Area(): float
TRIANGLE
+setFirst(in p:POINT): void +setSecond(in p:POINT) +setThird(in p:POINT)
ELIPSE
-Minor: float -Major: float +Area(): float
CIRCLE
-Radius: float +Area(): float
SQUARE
+setSideLength(in length:float): void
71
In particular, generalisation means that the objects of the child class can be used anywhere that the objects of the parent class can be used but not the converse! In the case of Single Inheritance a class may only inherit from from one, and only one, parent class. In the case of Multiple Inheritance a class may inherit from one or more parent classes. The parent class is called the Superclass and the child is called the Subclass. Figure 6.1 shows an inheritance hierarchy with the class Shape at the base of the hierarchy. Any of the classes that have Shape as the superclass can be used anywhere that Shape can be used. For example, the class Client has a method called clientFunction that takes an object of type Shape as a parameter. However, the actual parameter can be an object of type Shape, and object of type Polygon, an object of type Circle or indeed an object of any type that has Shape as one of its ancestors. This is the essence of Dynamic Binding, or as it is often called Polymorphism in object oriented languages. Associations are another kind of structural relationship that can occur between classes. If two classes have an association between them then you can Navigate between objects of those classes. An associations can between classes A and B can be implemented by: dening the instance variables of a class A to be of type B; passing objects of class A to the methods of class B; or sending messages between A and B.
WHOLE
PART
Figure 6.2: The Part-WHOLE relationship. Aggregation is an association between classes and objects that relates parts to their whole. An aggregation relation as in Figure 6.2 is implemented when objects of the class PART are stored in objects of the class WHOLE. The other relationships are typically between peers but the the aggregation relationship introduces a strict hierarchy between the whole and its parts.
72
the internal states of the object become relevant to the testing. As a consequence, the correctness of an object is not based only on the output given by method calls, but also on the internal state of the object. Methods are meaningless when separated from their class and treated in isolation. For example, a method may require an object to be in a specic state before it can be executed, where that state can only be set by another method (or combination of methods) in the class. Classes are thus the natural unit for testing Object Oriented Programs However, once this path has been taken then we will need to explore the effects of object oriented testing in the presence of: A. B. C. D. Encapsulation; Generalisation; Polymorphism; and Other associations.
73
Figure 6.3: A simple parent child relationship for re-testing. the subclass, but do we need to retest foo()? Suppose foo() contains the line X = X/intrange(); In this case foo() depends on child.intrange() and re-testing is necessary, especially because the new intrange can cause a divide by 0, but maybe we do not need to retest completely. Can tests for a parent class be reused for a child class? 74
First we need to observe that parent.range() and child.range() are two different functions with different specications and different implementations. Test cases are derived from the different specications, however, the functions are likely to be similar. Thus, if we minimise the overloading by using principles such as the open/closed principle in our design, then the greater the chance that the inherited methods will not need re-testing in the child context. The new tests that are necessary will be those for child.Intrange() requirements that are not satised by the parent.Intrange() tests. For example, consider the following program fragment in Figure 6.4. The tests for the parent class Parent { public void describeSelf() { if (val < 0) message(Less); else if (val == 0) message (Equal); else message(More); } } class Child { public void describeSelf() { if (val < 0) message(Less); else if (val == 0) message (Zero Equal); else { message(More); if (val == 42) message(Jackpot); } } }
Figure 6.4: What new tests are necessary? and child classes appear in the following table.
Value -1 0 1 42
75
Implications of Polymorphism
Consider the inheritance hierarchy in Figure 6.1. The implementation of Area() that actually gets called will depend on the state of the object and the runtime environment. In procedural programming, procedure calls are statically bound we know exactly what function will be called and when at compile time and further, the implementation of functions do not change (well, not unless there is some particularly perverse programming) at runtime. In the case of object oriented programming each possible binding of a polymorphic class requires a separate set of test cases. The problem for testing is to nd all such bindings after-all the exact binding used in a particular object instance may only be known at run-time. Dynamic binding also complicates integration planning. Many service and library classes may need to be built and tested before they can be integrated to test a client class. 76
There are a number of possible approaches to handling the explosion of possible bindings for a variable. One approach is to try and determine the number of possible bindings through a combination of static and dynamic program analysis. Consider the Client class in Figure 6.1. The problem from a testing point of view is to know which actual Shape object gets called at run-time. If an object or type Square is bound to the variable S in clientFunction then we would expect a different result for the Area() computation than if Circle were bound to S. If we instrumented the code for Shape and all of its descendents to reveal the types of the actual objects that are bound to S then we could use that information to determine the subset of the class hierarchy to test. Beware however, this approach is not foolproof and is biased heavily towards the data used to generate the bindings. Remark 19 On average, however, the number of bindings found in practice is 2.
public class Stack { public void Stack(); // Creates an empty stack with no // data values. public void Push(Object o); // Push a data object onto the stack. public void Pop(); // Remove the top element from the // the stack. public Object Top(); // Returns the value on top of the // stack. public boolean IsEmpty(); // Returns true if the stack is empty, that // is contains no values. } Figure 6.5: A Java stack class
Description An undened stack. A stack that is dened but is empty, that is, a stack with no values in the stack. A stack with at least one value in it. Table 6.1: States for the Java Stack class.
Push
Create
78
One method is to group stack states that enable the same set of operations into a single testing automaton state. For example, in the stack example above we have collected all non-empty stacks into a single testing automaton state which we have called State 3 above.
79
Object
Shape
Polygon
Curved
Rectangle
Elipse
Bezier Curve
Figure 6.7: Inheriting from the Object type. code to all of the descendents of some part of the state, as we needed to in Figure 6.7 then implicit testing can be used. The idea is to test each of the transitions in the testing automaton. The methods in the class can be divided into: Constructors Extenders Transformers Observers The constructors for the class, such as the Stack() method in the Stack class. Methods to add data to class state, such as the Push() method in the Stack class. Methods to change the state of the class, such as Pop() method in the Stack class. Methods to observe the class state, such as the IsEmpty() method and the Top() in the Stack class.
Table 6.2: One classication of the methods of a class. For the Stack automaton in Figure 6.6 we could derive a set of test cases as follows. Testing the Stack constructors and observers: A place to begin is to gain assurance that the stack constructors work properly. The sequence of method calls: Stack(); IsEmpty() should return TRUE and Stack90; Top() 80
should return an exception or an error message. If the rst sequence does not return TRUE then there is a fault in either the IsEmpty() method or the Stack() method. If the second sequence does not return an exception or an error message then there is a problem with Stack() or Top(). If we cannot trust the observers, then its time to instrument the code and determine the exact nature of the fault. Testing the Stack extenders and observers: Since all stacks can be built up using just the stack constructor and the Push() operation then we can test Push() next. The rst problem here is that the input domain for Push() is Object2 . Object in Java is the type that is the parent of every other type and only contains a single public type. The specication for Push() does not require us to manipulate objects of type Object and so we can choose any actual type to test out Push(); in our case int is convenient. Given that we can trust IsEmpty() and Top() then the sequence Stack(); Push( 3 ); IsEmpty() should return FALSE and Stack(); Top() should return 3. If they do not then there is a fault in the Push() method and the return value of Top() may indicate the fault. Testing the Stack transformers and other operation: With condence in the constructors, observers and extenders we can set up sequences to test the other operations, for example, the sequence of method calls Stack(); Push( 3 ); Push( 4 ); Pop(); Top() should return 3. Given that we are condent in Stack(), Push() and Top() then the fault lies in Pop(). Remark 20 (i) Each of the test cases above consists of a sequence of method calls rather than just a single call. (ii) It is possible that a test case may consist of an equality between two sequences of method calls, for example, Stack(); Push( 3 ); Pop(); == Stack(); if such a test for equality can be made. (iii) If methods exist for forcing a class into one of the testing states then this can make testing long sequences of method calls easier. The design of object oriented system is a very important part of the testing methodology. Designing object oriented programs for testing is just as important as designing object oriented systems to meet their requirements even though there may be a tension between the two. Fortunately, for state based testing state models are often created as part of a design methodology. For example, UML uses state-charts precisely for the purpose of specifying and understanding the legal sequences of actions on an object.
2
81
6.5 Black Box and White Box Testing for Object Oriented Programs
We conclude this chapter with a brief look at how our existing testing methods for imperative programs can be applied in Object Oriented programs.
Black-box Testing Conventional black-box methods are useful for object-oriented systems! We dont need the details of the internal states for objects and can rely on the interface of a class and the specications for the class.
White-box Testing White-box techniques can be adapted to method testing, but are not sufcient for testing classes. The reason for this is that methods can call each other within a class and that they interact through the class state. In this case the coverage criteria can become dependent on the internal state. In turn the state of an object may well depend on the preceding sequence of object method calls and their parameter values.
detailed (lowest level) of the architecture specication. In practice, integration testing means understanding how objects interact! It also means testing specic contexts such as dynamically bound variables and parameters, and polymorphic operators. There are a number of building strategies that help to systematically integrate and test object oriented programs. Thread-based The idea is to incrementally build up threads that respond to some input or event. A thread consists of all the classes needed to respond to a set of related external inputs or events. Each class is unit tested, and then the set of classes in the thread is exercised. Uses-based The idea is to begin by testing classes that use few or no other classes, then, test classes that use the rst group of classes, then follow this by testing the classes that use the second group, and so on. Regression testing is all about re-testing xed or modied code and ensuring that this is done as quickly as possible. Of note is that changes may have greater impact because of inheritance problems discussed earlier. System testing focuses on the behaviour of the system as a whole. Tests are derived from the requirements specications and the the system is often tested as a Black-Box. Drivers and stubs are usually not needed though automation eases re-testing.
83
Chapter 7
84
Chapter 8
85
Chapter 9
Software Safety
86
Chapter 10
Software Security
87
Chapter 11
88
Chapter 12
Portability
89
Chapter 13
Concluding Remarks
90