Testing and inheritance

Instructor's Guide


intro, methods, objects, contracts, formal, summary, Q/A, literature
One of the most prominent claims made by adepts of an object-oriented approach is that code may easily and reliably be reused, even without access to the source code. This claim suggests that the inherited part of the code need not be re-tested. An example will be given, however, showing that this is only partially true. See slide 4-inheritance. Like most such examples, it is a contrived one, but what it shows is that the correct behavior of a class can depend upon accidental properties of the class that may no longer hold when the code is being reused in a different context.

Testing and inheritance

Because


slide: Testing and inheritance

As a general rule, inherited code must be re-tested. One reason for this is that a subclass may affect inherited instance variables. This is a problem especially when using a language that does not provide encapsulation for derived classes, such as Eiffel. However, in Eiffel appropriate pre-conditions can save you from violation by derived classes. In contrast, C++ does allow such encapsulation (by means of the keyword private), but inherited instance variables may still be accessed when they are declared protected or when a method returns a (non const) reference. See section 2-references. Another reason not to assume that inherited code is reliable is that the inherited class may employ virtual functions which may be redefined by the derived class. Redefining a virtual function may violate the assumptions underlying the definition of the base class or may conflict with the accidental properties of the base class, resulting in erroneous behavior.

Example -- violating the invariant

The example shown below illustrates that redefining a virtual function, even in a very minor way, may lead to a violation of the invariant of the base class. Actually, the invariant ( n >= 0 ) is an accidental property of the class, due to the fact that the square of both positive and negative numbers is always positive.
  class A {  
invariant A: n >= 0
public: A() { n = 0; } int value() { return next(n); } void strange() { next(-3); } protected: virtual int next( int i ) { return n = n + i * i; } int n; };
  class B : public A { 
not invariant A
public: B() : A() { } protected: virtual int next( int i ) { return n = n + (n + 1) * i; } };

slide: Violating the invariant

Testing instances of class A will not reveal that the invariant is based on incorrect assumptions, since whatever input is used, invoking value() will always result in a positive number. However, when an instance of B is created, invoking strange() will result in an error.

Test cases

  A* a = new A; a->value(); a->strange(); a->value(); 
ok
A* b = new B; b->value(); b->strange(); b->value();
error

Dynamic binding

  int f(A* a) {
  	a->strange();
  	return a->value();
  }
  

slide: Test cases

The example illustrates what happens when instances of a derived class (B) are behaviorally not conforming with their base class (A). The penalty of non-conformance is, as the example clearly shows, that functions defined for inputs of the base class no longer behave reliably, since instances of derived classes (although legally typed) may violate the assumptions pertaining to the base class.

As an aside, it should be noted that the problems illustrated above would not have occurred so easily if the invariant and the behavior of the base and derived classes had been made explicit by means of a client-server contract. Moreover, annotating the methods with the proper pre- and post-conditions would allow automatic monitoring of the runtime consistency of the objects.