Derived Tester Classes

ECE 250 students are not required to know this.

The Tester class provides the general framework for implementing an interpreter which can be used to test a particular class, however, a derived class (subclass) specific to the class being tested must be created.

Only two member functions of the Tester class must be overloaded: the constructor and void process().

The general form of such a class is as follows:

/*************************************************
 * ClassNameTester
 * A class for testing the class ClassName.
 *
 * Author:  Douglas Wilhelm Harder
 *************************************************/

#ifndef CLASSNAMETESTER_H
#define CLASSNAMETESTER_H

#include "Tester.h"
#include "ClassName.h"
// other necessary include files

#include <iostream>

using namespace std;

class ClassNameTester:public Tester<ClassName> {
    public:
        ClassNameTester( ClassName *obj = 0, int c = 1 ):
            Tester<ClassName>( obj, c ) {
            // empty constructor
        }
};

/****************************************************
 * void process()
 ****************************************************/

void Tester<ClassName>::process() {
    if ( command == "command_1" ) {
        // do something
    } else if ( command == "command_2" ) {
        // do something
        } else if ( command == "command_3" ) {
           .  
           .  
           .  
    } else {
        std::cout << command <<: "Command not found." << std::endl;
    }
}
#endif

The void process() function checks the Tester class member variable command and compares it with commands which specifically test the current class ClassName. If the command does not match any of the exected class-specific identifiers, a standard warning is printed to the console. Note that the warning is similar to the warning printed by Unix.

The object currently being tested refernced by the pointer object which is a member of the Tester class. Thus, any functions to be evaluated on this instance of ClassName should be envoked with object -> function( ... ).

Individual Functionality Testers

Each instruction issued by the user calls one of the member functions in the class being tested (this includes constructors and destructors). Additional arguments are then read from console input and the appropriate function call is made.

Each test on a member function of a given class must be different, however, there experience has shown that the method of testing a function usually falls into one of five classifications:

  1. Tests which always succeed,
  2. Tests which conditionally pass depending on return values,
  3. Tests which print to console output,
  4. Tests which call alternate testers, and
  5. Tests which expect errors.

These are expanded upon in the next five sections.

1. Test which Always Succeed

In some cases, it is impossible to determine if a command succeeded. For example, inserting an element into a linked list should always succeed, and therefore it would be pointless to have the function return a Boolean value. Such member functions simply always print Okay and must be tested at some point in the future using other accessors.

    } else if ( command == "push_front" ) {
        int n;
        cin >> n;   // read argument from console in

        object->push_front( n );
        std::cout << "Okay" << std::endl;

2. Tests Which Conditionally Pass

It is important that whichever member function is being tested that it is only called once. As such, it is useful to have at least two local variables: expected_X and actual_X. The expected result is read from console in, while the actual result is the return value of the member function.

If the test fails, a reasonable error message which displays:

A sample of such a test is provided here.

    } else if ( command == "size" ) {
        int expected_size;
        cin >> expected_size;

        int actual_size = object->size();

        if ( actual_size == expected_size ) {
            std::cout << "Okay" << std::endl;
        } else {
            std::cout << "Failure in size(): expecting the value '"
                      << expected_size << "' but got '"
                      << actual_size << "'" << std::endl;
        }

Thus, as sample error message would be

Failure in size():  expecting the value '3' but got '7'

3. Tests Which Call Printing Functions

If a test calls a function which, rather than returning a value, prints to the screen, the form is quite straight-forward, as is demonstrated:

    } else if ( operation == "topological_sort" ) {
        object->topological_sort();
        std::cout << std::endl;

The terminating print of an end-of-line character is unnecessary if the function is expected to print one.

4. Tests Which Call Other Derived Tester Classes

Consider, for example, a test which needs to call another testing class. For example, calling the head() function of a singly-linked list returns a pointer to the first node of the linked list. To test this object, we must create a tester for a SingleNode class. This may be done as demonstrated:

    } else if ( command == "head" ) {
        SingleNode<int> *actual_head = object->head();

        if ( actual_head == 0 ) {
            std::cout << "Failure in head(): expecting a non-null head pointer"
                      << std::endl;
        } else {
            std::cout << "Okay" << std::endl;

            SingleNodeTester tester( actual_head );

            tester.run();
        }

First we check that the head is not a 0 pointer, and then we create an instance of a SingleNodeTester for that particular class. All subsequent commands are passed to the instance of the SingleNodeTester until the user types the command exit.

Another example is a test of the assignment operator:

    } else if ( command == "assign" ) {
        SingleList<int> new_object = *object;

        std::cout << "Okay" << std::endl;

        SingleListTester tester( &new_object );

        tester.run();

A new object is created, it is assigned the current object being tested, and this is used to create a new tester.

5. Tests Which Call Expect Exceptions

Consider trying to access the first element (the front) of an empty linked list. This would generate an undeflow exception. To test this, we could explicitly check for an exception. This is demonstrated here:

    } else if ( command == "front_exception" ) {
        try {
            int actual_front = object->front();
            std::cout << ": Failure in front(): expecting an exception but got '"
                      << actual_front << "'" << std::endl;
        } catch ( underflow ) {
            std::cout << "Okay" << std::endl;
        }

Copyright ©2006 by Douglas Wilhelm Harder. All rights reserved.