Adding Exception Testing to Unit Tests

Exceptions can add a bewildering number of potential execution paths to otherwise simple code. Here is a way to test those extra paths without writing a bazillion test cases.


April 01, 2001
URL:http://drdobbs.com/adding-exception-testing-to-unit-tests/184401380

April 2001/Adding Exception Testing to Unit Tests/Figure 1

Figure 1: The design of the exception testing system

April 2001/Adding Exception Testing to Unit Tests/Listing 1

Listing 1: A class for counting tests passed, tests failed, number of exception points, and which exception point to fail on

#ifndef COUNTER_H
#define COUNTER_H

#include <exception>
#include <iostream>
#include <typeinfo>
#include <strstream>

class TestException : public std::exception
{
    virtual const char* what() const throw() {
        return "TestException";
    }
};

class Counter {
private:
    // This variable indicates the number of the exception point
    // that we are up to executing in the code.
    static int sExceptionPointCount;
    // The number of the exception point which should throw.
    static int sThrowCount;
    static bool sDontThrow;

    // This counts the number of times new has successfully returned
    // a non-null result, minus the number of times a non-null ptr
    // has been passed to delete.
    static int sNewCount;

    static int sPassCount;
    static int sFailCount;

    static std::ostream* mOs;

public:
    // This function should be called wherever an
    // exception could be generated in the real code.
    static void CouldThrow() throw ( TestException );

    // Functions for the test harness to use.
    static void SetThrowCount( int inCount ) throw();
     // Prevents throwing until next call to SetThrowCount()
    static void DontThrow() throw();
    static bool HasThrown() throw();

    // Return the number of currently allocated blocks.
    static int GetAllocationCount() throw();

    // Managing tests.
    static void Pass( const char* testName ) throw();
    static void Fail( const char* testName ) throw();
    static void Test( bool result, const char* testName ) throw();
    static void PrintTestSummary();

    // Managing where the output is sent. The default is cout.
    static void SetOutputStream( std::ostream* os );
    static std::ostream& GetOut(){ return *mOs; }

    // For use by our definitions of new and delete.
    static void* MemAllocated( void* rawMem );
    static void DoDeallocate( void* userMem );
};

/************************************************
    TestDriver
    This method does the test. It will cause all 
    exception paths to be executed. 
    
    For example usage, see below.
*************************************************/

template<class TestFunction>
void TestDriver()
{
    int i = 0;
    int start_count, leaks;
    bool thrown;
    Counter::GetOut() << "Doing test " 
                      << typeid( TestFunction ).name() 
                      << std::endl;

    do {
        start_count = Counter::GetAllocationCount();

        Counter::SetThrowCount( ++i );
        //Counter::GetOut() << std::endl 
        //    << "        Exception count: " << i << std::endl;
        try {
            TestFunction::DoTest();
        }
        catch( const TestException& e ) {
        }
        catch( const std::bad_alloc& e ) {
        }
        catch( ... ) {
        }

        thrown = Counter::HasThrown();
        // Should set not to throw exceptions for the stuff below.
        Counter::DontThrow();
        
        // Check for resource leaks here. 
        // ...

    } while( thrown );
    // Loop terminates when we make it all the way
    // through a test without triggering the exception counter.
    Counter::GetOut() << "            " << i 
                      << " execution paths were tested." 
                      << std::endl;
}

/*******************
To use this TestDriver function, supply a class like this:

class TestPath1
{
public:
    static void DoTest() {
        // Test some aspect of your class or function here,
        // by calling it with known inputs and checking the results.
        // For example:
        
        // This tests the conversion constructor of a 
        // String class (assuming that operator== works OK).
        String s1(""), s2("Hello");
        Counter::Test( s1 == "", "Correct string value" );    
        Counter::Test( s2 == "Hello", "Correct string value" );    
    }
};

Then, supply a main like this

int main()
{
    TestDriver<TestPath1>();
    TestDriver<TestPath2>();
    // and so on...
    
    Counter::PrintTestSummary();
    return 0;
}

********************/

#endif // COUNTER_H
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 2

Listing 2: Implementation of Counter class

#include "Counter.h"
#include <new>
#include <cstdlib>
#include <cassert>

using namespace std;

int Counter::sExceptionPointCount = 0;
int Counter::sThrowCount = -1;
bool Counter::sDontThrow = false;
int Counter::sNewCount = 0;
int Counter::sPassCount = 0;
int Counter::sFailCount = 0;
ostream* Counter::mOs = &cout;

void Counter::CouldThrow() throw ( TestException )
{
    if( !sDontThrow &&
        ++sExceptionPointCount == sThrowCount ) 
    {
        throw TestException();
    }
}

void Counter::SetThrowCount( int inCount ) throw()
{
    sExceptionPointCount = 0;
    sThrowCount = inCount;
    sDontThrow = false;
}

void Counter::DontThrow() throw()
{
    sDontThrow = false;
}

bool Counter::HasThrown() throw()
{
    return sExceptionPointCount >= sThrowCount;
}

int Counter::GetAllocationCount() throw()
{
    return sNewCount;
}

void Counter::Pass( const char* testName ) throw()
{
    ++sPassCount;
    //(*mOs) << "        Passed test " << testName << endl;
}

void Counter::Fail( const char* testName ) throw()
{
    ++sFailCount;
    (*mOs) << "****    Failed test " << testName 
        << " at exception count " << sThrowCount
        << "." << endl;
}

void Counter::Test( bool result, const char* testName ) throw()
{
    if( result ) {
        Pass( testName );
    } else {
        Fail( testName );
    }
}

void Counter::PrintTestSummary()
{
    (*mOs) << "Test Results:" << std::endl;
    (*mOs) << "Total Tests: " << sPassCount + sFailCount 
           << std::endl;
    (*mOs) << "Passed     : " << sPassCount << std::endl;
    (*mOs) << "Failed     : " << sFailCount << std::endl;
}

// not shown: debugging
// memory manager -- available in
// online version of this listing
// ...
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 3

Listing 3: David Reed’s original stack class, modified to account for operator new throwing a bad_alloc exception instead of returning a zero

#ifndef STACK_H
#define STACK_H

template<class T>
class Stack 
{
    unsigned nelems;
    int top;
    T* v;
public:
    unsigned Size();
    void Push(T);
    T Pop();

    Stack();
    ~Stack();
    Stack( const Stack& );
    Stack& operator=(const Stack&);

    bool Consistent() const;
};

template<class T>
Stack<T>::Stack()
{
    top = -1;
    v = new T[nelems=10];
}

template<class T>
Stack<T>::Stack( const Stack<T>& s )
{
    v = new T[nelems = s.nelems];
    if( s.top > -1 ) {
        for(top = 0; top <= s.top; top++) {
            v[top] = s.v[top];
        }
        top--;
    }
}

template<class T>
Stack<T>::~Stack()
{
    delete[] v;
}

template<class T>
void Stack<T>::Push( T element )
{
    top++;
    if( top == nelems-1 ) {
        T* new_buffer = new T[nelems+=10];
        for( int i = 0; i < top; i++ ) {
            new_buffer[i] = v[i];
        }
        delete [] v;
        v = new_buffer;
    }
    v[top] = element;
}

template<class T>
T Stack<T>::Pop()
{
    if( top < 0 ) {
        throw "pop on empty stack";
    }
    return v[top--];
}

template<class T>
unsigned Stack<T>::Size()
{
    return top+1;
}

template<class T>
Stack<T>& 
Stack<T>::operator=( const Stack<T>& s )
{
    delete [] v;
    v = new T[nelems = s.nelems];
    if( s.top > -1 ) {
        for(top = 0; top <= s.top; top++) {
            v[top] = s.v[top];
        }
        top--;
    }
    return *this;
}

template<class T>
bool Stack<T>::Consistent() const
{
    // Weak check
    return (top < int(nelems)) 
           and (v != 0);
}

#endif // STACK_H

— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 4

Listing 4: A class that can throw exceptions from any possible user-defined function

#ifndef TESTCLASS_H
#define TESTCLASS_H

#include "Counter.h"

class TestClass {
public:
    TestClass( int inVal = 0 ) 
    :    mVal( inVal )
    { 
        Counter::CouldThrow();
    }

    TestClass( const TestClass& other ) 
    :    mVal( other.mVal )
    { 
        Counter::CouldThrow(); 
    }

    ~TestClass() {}

    TestClass& operator=( const TestClass& other )
    { 
        Counter::CouldThrow();
        mVal = other.mVal;
        return *this;
    }

    bool operator==( const TestClass& other ) { 
        Counter::CouldThrow();
        return other.mVal == this->mVal;
    }
    
    bool operator!=( const TestClass& other ) {
        Counter::CouldThrow();
        return other.mVal != this->mVal;
    }
        
    bool operator<( const TestClass& other ) {
        Counter::CouldThrow();
        return other.mVal < this->mVal; 
    }

    bool operator<=( const TestClass& other ) {
        Counter::CouldThrow();
        return other.mVal <= this->mVal;
    }
// not shown: other operators
// ...

    // Don't need to overload class operator new,
    // because we already required that the global
    // operator new be overloaded to call 
    // Counter::CouldThrow().

    // See online version of this listing for discussion of
    // remaining recommended functions to be implemented
private:
    int mVal;
};

// I have considered that it is not really desireable to add any 
// more functionality to this class which might result in silent
// automatic conversions etc. If you need a particular method,
// you may wish to add it.


#endif // TESTCLASS_H
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 5

Listing 5: Test harness for Stack.h

#include "Counter.h"
#include "TestClass.h"

#if defined( STACK_REED )
#include "Stack_Reed.h"
#elif defined( STACK_REED_FIXED )
#include "Stack_Reed_Fixed.h"
#elif defined( STACK_SUTTER_1 )
#include "Stack_Sutter_1.h"
#elif defined( STACK_SUTTER_3 )
#include "Stack_Sutter_3.h"
#else
    #error "Don't know which stack to include."
    // #include "Stack_Reed.h"
#endif

typedef TestClass element_type;
typedef Stack<element_type> TestType;

class TestConstructDestruct
{
public:
    static void DoTest() {
        TestType a;
        Counter::Test( 0 == a.Size(), "Default constructor" );
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

class TestCopyConstruct1
{
public:
    static void DoTest() {
        TestType a;
        {
            TestType b( a );
            Counter::Test( 0 == b.Size(), "Copy empty" );
            Counter::Test( b.Consistent(), "b internal state" );
        }
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

class TestCopyConstruct2
{
public:
    static void DoTest() {
        TestType a;
        a.Push( element_type() );
        a.Push( element_type() );
        {
            TestType b( a );
            Counter::Test( 2 == b.Size(), "copy with 2 elements" );
            Counter::Test( b.Consistent(), "b internal state" );
        }
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

class TestAssign1
{
public:
    static void DoTest() {
        TestType a;
        TestType b;
        b = a;
        Counter::Test( 0 == b.Size(), "Assign empty" );
        Counter::Test( a.Consistent(), "a internal state" );
        Counter::Test( b.Consistent(), "b internal state" );
    }
};

class TestAssign2
{
public:
    static void DoTest() {
        TestType a;
        TestType b;
        a.Push( element_type() );

        a.Push( element_type() );
        b.Push( element_type() );
        b = a;
        Counter::Test( 2 == b.Size(), "Assign 2 elements" );
        Counter::Test( a.Consistent(), "a internal state" );
        Counter::Test( b.Consistent(), "b internal state" );

        a = a;
        Counter::Test( 2 == a.Size(), "Assign a to self" );
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

class TestPush
{
public:
    static void DoTest() {
        // This tests that the class obeys commit or rollback 
        // semantics.
        TestType a;
        for( int i = 0; i < 12; ++i ) {
            try {
                a.Push( element_type() );
                Counter::Test( i+1 == a.Size(), "push 1 element" );
            }
            catch( ... ) {
                Counter::Test( i == a.Size(), 
                    "push 1 element recover from exception" );
                Counter::Test( a.Consistent(), 
                    "a internal state after exception" );
                throw;
            }
        }
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

#ifdef STACK_HAS_TOP
class TestTop
{
public:
    static void DoTest() {
        TestType a;
        try {
            element_type b = a.Top(); // Should throw.
            Counter::Fail( "Top of empty" );
        } 
        catch( const char* ) {
            Counter::Pass( "Top of empty" );
        }
        a.Push( element_type() );
        try {
            element_type b = a.Top(); // Should not throw.
            Counter::Pass( "Top of non-empty" );
        }
        catch( const char* ) {
            Counter::Fail( "Top of non-empty" );
        }
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

class TestPop
{
public:
    static void DoTest() {
        TestType a;
        a.Push( element_type(1) );
        a.Push( element_type(2) );
        a.Pop();
        Counter::Test( 1 == a.Size(), "Pop reduces size" );
        Counter::Test( element_type(1) == a.Top(), 
            "Correct top element" );
        a.Pop();
        Counter::Test( 0 == a.Size(), "Pop reduces size" );
        try {
            a.Pop();
            Counter::Fail( "Pop empty" );
        }
        catch( const char* ) {
            Counter::Pass( "Pop empty" );
        }
        Counter::Test( a.Consistent(), "a internal state" );
        Counter::Test( 0 == a.Size(), "a correct size" );
    }
};

#else // STACK_HAS_TOP

// Test old style pop which returns the popped element.
// Note that it's impossible to pass these tests, which is why
// the newer version changes the pop method to return nothing.

class TestPop
{
public:
    static void DoTest() {
        TestType a;
        element_type b(0);
        a.Push( element_type(1) );
        a.Push( element_type(2) );
        try {
            b = a.Pop();
            Counter::Test( 1 == a.Size(), "Pop reduces count" );
            Counter::Test( element_type(2) == b, 
                "Correct pop value" );
        } catch(...) {
            // This test will get confused with the 
            // assignment failing.
            Counter::Test( 2 == a.Size(), 
                "Pop rollback on exception 1" );
            // Can't test value of stack top.
            throw;
        }
        try {
            a.Pop();
            Counter::Test( 0 == a.Size(), "Pop reduces count" );
        } catch( ... ) {
            Counter::Test( 1 == a.Size(), 
                "Pop rollback on exception 2" );
            throw;
        }
        try {
            a.Pop();
            Counter::Fail( "Pop empty throws exception" );
        }
        catch( const char* ) {
            Counter::Pass( "Pop empty throws exception" );
            Counter::Test( 0 == a.Size(), "Pop empty stays empty" );
        }
        Counter::Test( a.Consistent(), "a internal state" );
    }
};

#endif // STACK_HAS_TOP

int main( int argc, char* argv[] )
{
    TestDriver<TestConstructDestruct>();
    TestDriver<TestCopyConstruct1>();
    TestDriver<TestPush>();
    TestDriver<TestCopyConstruct2>();
    TestDriver<TestAssign1>();
    TestDriver<TestAssign2>();
#ifdef STACK_HAS_TOP
    TestDriver<TestTop>();
#endif
    TestDriver<TestPop>();
    TestDriver<TestAssign2>();

    Counter::PrintTestSummary();
    
    return 0;
}
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 6

Listing 6: Fixed version of David Reed’s stack class from Listing 3

#ifndef STACK_H
#define STACK_H

#include <algorithm> // for swap

template<class T>
class Stack 
{
    unsigned nelems;
    int top;
    T* v;
public:
    unsigned Size();
    void Push(T);
    T Pop();

    Stack();
    ~Stack();
    Stack( const Stack& );
    Stack& operator=(const Stack&);

    bool Consistent() const;
};

template<class T>
Stack<T>::Stack()
{
    top = -1;
    v = new T[nelems=10];
}

template<class T>
Stack<T>::Stack( const Stack<T>& s )
{
    v = new T[nelems = s.nelems];
    if( s.top > -1 ) {
        // 20000408 bstanley fix 7 cater for 
        // exceptions in T::operator=
        try {
            for( top = 0; top <= s.top; top++ )
                v[top] = s.v[top];
        } catch(...) {
            delete [] v;
            throw;
        }

        top--;
    } else {
        // 20000408 bstanley fix 1
        top = -1;
    }
}

template<class T>
Stack<T>::~Stack()
{
    delete[] v;
}

template<class T>
void Stack<T>::Push( T element )
{
    top++;
    // 20000408 bstanley fix 6 - commit or rollback semantics.
    try {
        if( top == nelems-1 ) {
            T* new_buffer = new T[nelems+=10];
            // 20000408 bstanley fix 5 - 
            // added exception handling to prevent leak.
            try {
                for( int i = 0; i < top; i++ ) {
                    new_buffer[i] = v[i];
                }
            } catch( ... ) {
                delete [] new_buffer;
                throw;
            }
            delete [] v;
            v = new_buffer;
        }
        v[top] = element;
    } catch( ... ) {
        --top;
        throw;
    }
}


template<class T>
T Stack<T>::Pop()
{
    if( top < 0 ) throw "pop on empty stack";
    return v[top--];
}

template<class T>
unsigned Stack<T>::Size()
{
    return top+1;
}

template<class T>
Stack<T>& Stack<T>::operator=( const Stack<T>& s )
{
    // 20000408 bstanley fix 3
    if( this != &s ) {
        // 20000408 bstanley fix 4 re-written to be exception safe.
        T* v_new = new T[nelems = s.nelems];
        if( s.top > -1 ) {
            try {
                for( top = 0; top <= s.top; top++ ) {
                    v_new[top] = s.v[top];
                }
                --top;
            } catch(...) {
                delete [] v_new;
                throw;
            }
        } else {
            // 20000408 bstanley fix 2
            top = -1;
        }
        // now swap
        swap( v, v_new ); // can't throw.
        delete [] v_new;
    }
    return *this;
}

template<class T>
bool
Stack<T>::Consistent() const
{
    // Weak check
    return (top < int(nelems)) and (v != 0);
}

#endif // STACK_H
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests/Listing 7

Listing 7: Herb Sutter’s stack class — first of three presented in his book, Exceptional C++

#ifndef STACK_H
#define STACK_H

#include <cassert>
#include <algorithm> // for swap
#include <cstdlib> // for size_t

#define STACK_HAS_TOP

template <class T> 
class Stack
{
public:
    Stack();
    ~Stack();
    Stack( const Stack& );
    Stack& operator=( const Stack& );
    std::size_t Size() const;
    void Push( const T& );
    const T& Top() const;  // if empty, throws exception.
    void Pop(); // if empty, throws exception.

    // Just checks that the internal representation 
    // is consistent, ie not destroyed.
    bool Consistent() const;
private:
    std::size_t vsize_;
    T* v_; // ptr to a memory area big enough for 'vsize_' Ts
    std::size_t vused_; // # of T's actually in use

    T* NewCopy( const T* src,
        std::size_t srcsize,
        std::size_t destsize );
};

template<class T>
Stack<T>::Stack():
    vsize_( 10 ), // initial allocation size
    v_( new T[vsize_] ),
    vused_( 0 ) // Nothing used yet
{}

template<class T>
Stack<T>::~Stack()
{
    delete [] v_;
}

template<class T>
Stack<T>::Stack( const Stack<T>& other ) :
    vsize_( other.vsize_ ),
    v_( NewCopy( other.v_,
        other.vused_,
        other.vsize_ ) ),
    vused_( other.vused_ )
{}

template<class T>
Stack<T>&
Stack<T>::operator=( const Stack<T>& other )
{
    if( this != &other ) {
        T* v_new = NewCopy( other.v_,
                other.vused_,
                other.vsize_ );
        delete [] v_; // this can't throw
        v_ = v_new;
        vsize_ = other.vsize_;
        vused_ = other.vused_;
    }
    return *this;
}

template<class T>
std::size_t Stack<T>::Size() const
{
    return vused_;
}

template<class T>
void Stack<T>::Push( const T& t )
{
    if( vused_ == vsize_ ) { // grow if necessary by some 
                             // grow factor
        size_t vsize_new = vsize_*2 + 1;
        T* v_new = NewCopy( v_, vsize_, vsize_new );
        delete [] v_; // this can't throw
        v_ = v_new; // take ownership
        vsize_ = vsize_new;
    }
    v_[vused_] = t;
    ++vused_;
}

template<class T>
const T& Stack<T>::Top() const
{
    if( vused_ == 0 ) {
        throw "empty stack";
    }
    return v_[vused_-1];
}

template<class T>
void Stack<T>::Pop()
{
    if( vused_ == 0 ) {
        throw "empty stack";
    }
    --vused_;
}

template<class T>
bool Stack<T>::Consistent() const
{
    // Weak check.
    return vused_ <= vsize_ and v_ != NULL;
}

template<class T>
T* Stack<T>::NewCopy( const T* src,
    std::size_t srcsize,
    std::size_t destsize )
{
    assert( destsize >= srcsize );
    T* dest = new T[destsize];
    try {
        copy( src, src+srcsize, dest );
    }
    catch(...) {
        delete [] dest;
        throw;
    }
    return dest;
}

#endif // STACK_H
— End of Listing —
April 2001/Adding Exception Testing to Unit Tests

Adding Exception Testing to Unit Tests

Ben Stanley

Exceptions can add a bewildering number of potential execution paths to otherwise simple code. Here is a way to test those extra paths without writing a bazillion test cases.


Introduction

Much water has gone under the bridge since Tom Cargill expressed reservations about the reliability of code that uses exception handling [1]. Tom pointed out that naive programming of exception handling typically leads to resource leaks and incoherent object states. Exceptions should arise only under extreme conditions such as low memory or lack of disk space, making exception related problems difficult to reproduce.

Testing is no substitute for writing robust and complete code. Before attempting to write code that must be exception safe, you should read Herb Sutter’s excellent book, Exceptional C++, for some robust exception handling techniques [2, Items 8-19]. Once the code is written, a solid testing regime can help flush out any remaining bugs, and increase confidence that the code operates correctly. Rigorous testing methods have been developed for normal execution paths [3, Chapter 25]. This article describes a simple method of adding exhaustive testing of the exception paths to the test suite.

Current testing methodology seeks to construct a set of tests to verify the integrity of a unit of software. Usually, this unit is a class, although it could be a group of collaborating classes. For the test to be thorough, there must be at least one test to exercise each path of execution through every function or class method in the unit. Thus, there are at least as many tests in the test suite as there are normal execution paths through the software, and usually more. If you seek to test exceptional paths in addition to normal execution paths, you will have to add more tests. How many? Consider the following code snippet:

String EvaluateSalaryAndReturnName(
  Employee e )
{
  if( e.Title()=="CEO" or
      e.Salary() > 100000 )
  {
    cout << e.First() << " "
         << e.Last()
         << " is overpaid" << endl;
  }
  return e.First()+" "+e.Last();
}

Herb Sutter claims that there are no fewer than 20 possible exceptional code paths through this function, compared to the three normal paths [2, Item 18]. That’s a lot of extra paths to test! Even worse, it is less than obvious what these paths are, or how to cause them to execute. Luckily, it is possible to test these 20 extra exceptional paths using only the test suite for the three normal paths, and some extra template code, which is shared by all your tests.

A simplified outline of the method is as follows:

I demonstrate this method of exception testing by three different examples, which use a common framework for performing the testing.

Design of the Exception Testing System

The design of the exception testing system is shown in Figure 1. Boxes indicate code modules. Boxes with single borders are generic and do not change for testing separate systems — these modules are supplied in the online source code that accompanies this article (see www.cuj.com/code). The boxes with double borders indicate code that must be supplied by the user. The arrows indicate function call dependencies between the code modules. The dotted arrow indicates a function call dependency that is usually not necessary.

The central idea in the design is the enumeration of the exception points. This is done by placing a call to the exception point counter class at each point in the code where an exception may directly arise. The exception counter class maintains two counts: the number of the current exception point, sExceptionPointCount, and the number of the exception point where a test exception should be thrown, sThrowCount. The code under test calls function CouldThrow at each point where an exception could be thrown. The CouldThrow method will throw an exception if it is at the right exception point. The test harness driver has a loop which enumerates all the exception points in turn, by first calling SetThrowCount and then calling the unit test in the test harness.

Since most exception points are due to memory management problems, we can take a giant shortcut by replacing the default implementation of operator new and operator delete with debugging versions that have been modified to call CouldThrow every time new is called. In many cases, this shortcut eliminates the need to write a debugging version of the classes used by the code under test.

Some exception points will be impractical to instrument with a call to CouldThrow. Such exception points may alternatively be exercised by designing the test suite so that it sets up the conditions that cause the exception as one of the test cases. This will still cause the exception code path to be tested.

Any remaining exception points that are not exercised by calling CouldThrow or explicitly tested by the test suite will not be tested and are deemed to be outside the domain of the test.

Implementation

The TestDriver Function

The TestDriver template function causes all the exception points in a test to actually throw an exception, one at a time. It requires a functor class containing one method with the signature static void DoTest(). This function is assumed to perform a sequence of tests to exercise one aspect of a class’s functionality:

template<class Func>
void TestDriver()
{
  int i = 0;
  do {
    // ...
    Counter::SetThrowCount( ++i );
    try {
      Func::DoTest();
    }
    catch( TestException& e ){}
    catch( bad_alloc& e ) {}
    catch( ... ) {
      Counter::Fail(
        "Bad Exception");
    }
    // ...
  } while( Counter::HasThrown() );
  Counter::SetThrowCount( -1 );
}

A more complete version of this function is shown in Listing 1 (Counter.h). The loop enumerates the exception points, using i as the counter. The call to SetThrowCount at the top of the loop instructs the Counter class to throw an exception at the specified exception point. Testing starts at the first exception point. Then, the function under test is called, inside a try/catch block. It is expected that the function will throw an exception. The Counter::CouldThrow method throws a TestException. However, operator new translates this into a bad_alloc exception for testing purposes, so we have to be prepared to deal with that here. Any other kind of exception that is triggered by bad inputs should be caught by the test harness, so any other strange exceptions caught here cause a test fail. This should be changed if it is unsuitable for your application.

An interesting point here is the loop termination condition. There must be a finite number of exception points in a test function. When they have all been tested, the TestDriver function will return normally, testing the normal path of execution for that test as a by-product. In this case, the exception has not yet been thrown. This condition is detected by the Counter::HasThrown method, which is used to terminate the loop.

At the end of the exception tests, the throw point is set to -1 to prevent exceptions from being thrown in the testing program.

The parts of Listing 1 marked with an ellipsis (...) contain checks for memory leaks that may have occurred during a test. It is assumed that each test is self contained, and that no memory should remain allocated after the test. You can also walk the heap and check that everything is okay, if your debugging memory manager supports such things.

The ... areas also contain code to print out the number of exception paths that were tested.

Test Harness Functions

The test harness functions are supplied as functor objects. This allows automatic reporting of the name of the test set through the use of RTTI information. Each test harness functor should have the following form:

class TestDefaultConstruct {
public:
  static void DoTest() {
    String s;
    Counter::Test( s == "", "correct value" );
  }
};

The above code just tests the default constructor of a String class. It does so by constructing a String using the default constructor, and then testing its value against the expected value. The static function Counter::Test accepts a Boolean result from a test as its first argument — a true counts as a pass, false counts as a fail. The second argument is a description of the test. This is printed in the case of a failure, to help in diagnosing what happened.

A proper test harness will have one of these classes to test each aspect of functionality of your class or module. You could incorporate test data from a file, or just hard-code the expected results from simple hand-worked examples. The key point is to compare the actual state of the class against what you expect it should be, given the functions you called. If the test harness detects anything amiss, it will let you know.

Instrumenting the Code Under Test

This section refers to the code that you wish to demonstrate operates correctly in the presence of exceptions. You can test individual functions, simple classes, or template classes.

If you are testing a class, it is useful to add an extra debugging method, named Consistent, that checks whether the data members of the class are in a consistent state. This is also known as checking the class invariants. For example, if a stack class has a null pointer to its array element, but its size member says it has five elements, then the class state is inconsistent.

Instrumenting Classes

If your code under test uses any other classes, you may need to create debugging versions of them that call Counter::CouldThrow at every exception point. However, if the only exception points are calls to the standard operator new and operator delete, then you don’t have to change anything. By using overloaded versions of operator new and operator delete, the required calls to CouldThrow can be obtained without changing anything. Other exception points will have to be treated [on an individual basis?]

Exception Point Counting

The exception point counting code is at the bottom of the call graph diagram (Figure 1). It is really very simple:

class Counter {
private:
  static int sExceptionPointCount;
  static int sFailCount;
  // ...
public:
  static void CouldThrow() {
    if( ++sExceptionPointCount ==
      sFailCount )
        throw TestException();
  }
  static void SetThrowCount(int c){
    sTestCounter = 0;
    sFailCount = c;
  }
  // ...
};

The SetThrowCount method is used to set the number of the exception point that will throw. The CouldThrow method will throw the exception at the appropriate exception point. There is a lot of other functionality included in this class in the full version (see Listing 1, Counter.h and Listing 2, Counter.cpp); its operation should be self-evident from an inspection of the code.

A Debugging Memory Manager

I have included a debugging memory manager with the online listings for two reasons:

1) To find simple memory errors in the program under test; and

2) To allow calls to operator new and operator delete to be instrumented to call Counter::CouldThrow.

The overloading of operator new and operator delete is a convenience — these versions automatically call CouldThrow. This functionality is useful because the majority of exception points in a typical application are due to memory allocation.

The debugging memory manager used in this testing facility additionally maintains a linked list of all allocated blocks, and a count of how many have been allocated. This allows the program to check if any memory leaks have occurred within a single test, and to also check that pointers passed to delete are actually pointers to validly allocated blocks. This was the minimum necessary functionality to detect the bugs that are in the examples. A more professional debugging memory manager may be substituted, as long as its operator new and operator new[] functions can be modified to call Counter::CouldThrow. Techniques for writing debugging memory managers are discussed by Steve Maguire [4].

Test Examples

The following examples demonstrate how to use the testing method on a function and on a template class.

Testing a Function

The function to be tested is the EvaluateSalaryAndReturnName function mentioned in the introduction. In order to test it, you need a String class and an Employee class; these are provided with the online source listings.

There are three normal paths of execution through this code, so three test cases are needed to exercise it. A single test case is shown here, which forms one part of the test harness box in the design in Figure 1:

class TestPath1 {
  static void DoTest() {
    String s;
    s=EvaluateSalaryAndReturnName(
      Employee( "Homer", "Simpson",
        "Nuclear Plant Controller",
        25000 ) );
    Counter::Test(
      s == "Homer Simpson",
      "Correct return value" );
  }
};

Here I have constructed a test case which will fail to enter the if statement within the EvaluateSalaryAndReturnName function, so nothing should be printed. To fully test this function, the output would have to be redirected into an internal string buffer or file. This is possible on Unix by closing and reopening the standard output stream, but is left as an exercise for the reader. Our interest here is the exception paths that we can cause to execute through the function using the TestDriver function.

After constructing the test cases, a suitable main is needed to call them:

int main()
{
  TestDriver<TestPath1>();
  // TestDriver<TestPath2>();
  // ...
  Counter::PrintTestSummary();
  return 0;
}

A full test harness will call further tests. However, when this program is built and run, it prints the following output:

Doing test 9TestPath1
18 execution paths were tested.
Test results:
Total Tests: 19
Passed     : 19
Failed     : 0

(The strange test name is just how g++ prints out the class name when you use RTTI.) The important thing that the output shows here is that 18 execution paths were tested, even though there was only one normal path of execution through the code accessible to the test suite. This is due to all the exception points being tested, one by one. Each exception point gives rise to a separate execution path. The number is different from the claimed 20 possible paths for several reasons:

1) The test harness contains extra exception points outside the code under test. Thus, some execution paths only differ outside the function being testing here. This is acceptable, since we still know these paths are being tested.

2) The path that is being tested does not cover all the possible exception points in the function, since half of the if test and the body of the if statement are not executed. Thus, not all of the exceptional paths are revealed yet.

At this point, I should point out that e.Salary() might return a user-defined type. The comparison between this user-defined type and 100000 would be performed by a user-defined operator==, which could throw. This possibility is included in Herb Sutter’s claimed count of 20 exception points. However, this test program has not tested these things, because this particular program does not return a user-defined type from e.Salary(). The program has only tested what could actually throw.

A complete test set is supplied as part of the code with the article. While developing this test set, I found two paths of execution that neglect to call the String destructor. (This was verified by inspecting a function call trace.) These appear to be caused by bugs in the code generated by g++ 2.95.2! This testing methodology has also found bugs in other compilers.

Testing a Template Class

The template class to be tested here is the same one used in Tom Cargill’s article on exception handling — David Reed’s stack class [6]. This class is a good example for showing that the test method finds problems. The code is shown in Listing 3 (Stack_Reed.h). The original code assumed that new returned zero when it failed — I have changed the code to assume that new throws bad_alloc. I have changed a few method names to be consistent with Herb Sutter’s implementations, to allow a common test harness. I have also added a Consistent method, which checks on the internal consistency of the Stack’s data.

The template class, Stack, is instantiated with a template argument of type TestClass (see Listing 4, TestClass.h). TestClass behaves like a very temperamental integer — it can be copied, assigned to, added, and so on, all with the possibility of throwing an exception. The only method of TestClass that does not throw is the destructor. This throw capability is achieved by inserting a call to Counter::CouldThrow into each of the methods, thus allowing us to test the behavior of Stack under hostile conditions.

The complete stack test suite is shown in Listing 5 (TestStack.cpp). The following Stack test from the suite exposes a fault in the copy constructor:

class TestCopyConstruct2 {
public:
  static void DoTest() {
    Stack<TestClass> a;
    a.Push( TestClass(1) );
    a.Push( TestClass(2) );
    { Stack<TestClass> b( a );
      // ...}
    // ...              }   };

When this test is run, it produces the following output:

Doing test 18TestCopyConstruct2
****    Failed test Memory leak
  (1 block) at exception point 27.
****    Failed test Memory leak
  (1 block) at exception point 28.
29 execution paths tested.

It seems that there are two memory leaks when exceptions are thrown during the tests. To identify the cause of the leaks, the execution paths must be identified. The program may be converted so that only the faulty execution path is used, for the convenience of debugging. To do this, follow this procedure:

  1. Comment out all the tests except the one that causes the problem.
  2. Hard-code the exception point into the TestDriver function, using i = exceptionPoint-1 before the start of the do loop.
  3. Put a break before the while statement in the TestDriver function to prevent other tests from running.

This program will follow only the faulty execution path. This makes it easier to trace the program in the debugger to find where the leak occurs. The exact point where the exception is thrown may be found by placing a breakpoint on the throw statement in CouldThrow. To facilitate further testing, it is best to perform these modifications on a copy of the original files.

It turns out that these memory leaks are caused by an exception from TestClass::operator= while the elements are being copied from one array to the other in the Stack copy constructor. The memory for the array is not deallocated. The exception leaves the constructor, leaving the class only partially constructed, so the destructor is not called. The second leak is caused by having two elements on the stack to copy, so the same mechanism is repeated by throwing from the second assignment in a subsequent test. This fault can be fixed by inserting a try/catch block into the constructor to delete the array in the case of an exception during copying. See Listing 6 (Stack_Reed_Fixed.h) for the fixed version.

The test harness (Listing 5, TestStack.cpp) and exception testing system can be used to find a complete list of the problems with this Stack class. I have done this, and the results are shown in Table 1. There are comments in the fixed version of Stack indicating where each repair was made. With this number of faults, this class is probably better scrapped and rewritten according to the guidelines in Exceptional C++ [2]. Indeed, it is impossible to pass the test suite without changing the interface to the stack class (problem 8 from Table 1). One of the stack classes included in Exceptional C++ is included with this article as Listing 7 (Stack_Sutter_1.h). Another (Stack_Sutter_3.h) is included with the online sources. These classes have a Top method, and the Pop method has been modified so as not to return anything. These changes address problem 8 from Table 1. The same test suite can be used to test all these implementations. Both of Sutter’s implementations pass the test suite without modification.

Testing Exceptions Thrown by the Class Under Test

Now that I have discussed how to test exceptions caused by memory faults, I will turn to exceptions that are thrown by the class under test. How do we test these? By setting up the conditions that cause the exception to be thrown. For example, the Pop method of the Stack class throws an exception if you try to pop an empty stack. We can test this exception path by writing a test such as the following:

class TestPop {
public:
  static void DoTest() {
    Stack<TestClass> a;
    try {
      a.Pop();
      Counter::Fail("Pop empty");
    } catch(const char* ) {
      Counter::Pass("Pop empty");
    }
    Counter::Test( a.Consistent(),
      "a internal state");
    Counter::Test( 0 == a.Size(),
      "a correct size");
  }
};

This particular test tests Sutter’s version of the class, in which Pop has been modified not to return the popped element. The test sets up the stack class to be empty so that Pop will fail. The test is subsequently written so that Pop must throw an exception to pass the test. The stack must also subsequently have consistent internal state, and have zero size as well.

It would also have been possible to test this path of execution by placing a call to Counter::CouldThrow at the point where the Pop could fail. This would not have required such a carefully designed test suite. However, it would have required modification of the code undergoing testing, which is usually undesirable.

Discussion

Using this technique imposes the following requirements:

1) You must have the source code of the class or function under test. (Object code may be sufficient if the class does not use any templates.)

2) You must write an exhaustive test suite for the functionality of that class or function, including for any exceptions that the class or function itself may throw due to being misused in any way.

Meeting the above requirements allows us to:

1) Test whether the class or function under test leaks memory under any circumstances, including due to exception propagation;

2) Easily exercise exception handling code for exceptions that are caused by out-of-memory conditions; and

3) Exercise exception handling code for exceptions that are caused by misusing the class (and the misuse is included in the test suite).

Thus, this testing method is applicable to class-based testing or unit testing. It does not allow us to do any of the following:

1) Test pre-compiled binaries of libraries or complete programs; or

2) Test exceptions due to any reason other than memory failure or conditions deliberately set up by the test suite. An example of such conditions would be I/O faults. (I/O faults could be tested using this method if the I/O library were instrumented with calls to CouldThrow.)

Therefore this method, as it stands, is not applicable to integrated system testing.

This is not a perfect solution, but is much better than having no means of performing this testing at all.

Conclusion

This method allows you to test a function, normal class, or template class for its exception handling integrity. It requires some extra effort over that required to write a standard test suite. The tests must be crafted with some care for the results of the tests to be meaningful. However, if you go to the trouble of writing good tests and fixing any problems found with your code, you can be correspondingly more confident in your code. This technology has already been used to improve the quality of student assignments for simple classes. It has also uncovered incorrect exception handling code output from some compilers. This article only scratches the surface of what can be done by automated unit testing.

Acknowledgements

I gratefully thank Herb Sutter for encouraging me to write this article in the first place, and for assisting with reviewing it.

This technique was invented independently by Matt Arnold, and subsequently used by David Abrahams [7] to write a generic test suite for the STL [8].

References

[1] Tom Cargill. “Exception Handling: A False Sense of Security,” C++ Report, Vol. 6, No. 9, November-December 1994. Also available at http://meyerscd.awl.com/.

[2] Herb Sutter. Exceptional C++ — 47 Engineering Puzzles, Programming Problems, and Solutions (Addison-Wesley, 2000).

[3] Steve McConnel. Code Complete (Microsoft Press, 1993).

[4] Steve Maguire. Writing Solid Code (Microsoft Press, 1993).

[5] Scott Meyers. More Effective C++ (Addison-Wesley, 1996). Also available as a CD; see http://www.meyerscd.awl.com

[6] David Reed. “Exceptions: Pragmatic Issues with a New Language Feature,” C++ Report, October 1993.

[7] David Abrahams. “Exception Safety in Generic Components,” Dagstuhl Conference on Generic Programming, April 27 - May 1, 1998. Online at http://www.cs.rpi.edu/~musser/gp/dagstuhl/gpdag.html.

[8] David Abrahams and Boris Fomitchev, “Exception Handling Test Suite,” available at http://www.stlport.org/doc/eh_testsuite.html.

Ben Stanley graduated from the Australian National University with Honors in Theoretical Physics in 1994. He is now doing a PhD in Robotics at the University of Wollongong, Australia. He has lectured some first and second year C++ units. When he’s not busy writing his thesis, he makes puzzles.

April 2001/Adding Exception Testing to Unit Tests/Table 1

Table 1: Problems found with the original Stack class

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.