Boost logo

Boost :

From: Ullrich Koethe (koethe_at_[hidden])
Date: 2000-12-07 15:16:40


Hi there,

I followed the discussion about a unit test library and had a look at
"test_tools". I realized that this library's intend is very close to a
unit test library I've been using for a long time in my own projects,
with very good results. Thus, I'm submitting my library "unittest.h" for
consideration. The file is attached to the mail.

"unittest.h" was designed following the ideas of Kent Becks "JUnit"
(see "http://www.junit.org/"). It has the following features:

1. Each test executes some code and an arbitrary number of assertions
   that compare the outcomes with expected values.
2. A test suite is made up of an arbitrary number of (related) tests.
   Test suites can be nested into more complex test suites.
3. If an assertion fails, this particular test is aborted, and testing
   resumes with the next test.
4. Upon completion of all tests, a detailed test report (listing all
   failed assertions) is generated. If any test failed, the programm
   exits with code "1" (the exit code is used in makefiles).
5. Before each test, the environment is initialized into a known state
   (Beck calls this a "fixture"). This ensures that previous tests do
   not influence the outcome of the current test.
6. On UNIX and Windows (Visual C++ only), "unittest.h" not only handles
   assertions, but also catches signals from the OS
   (such as memory access violation).

Typical use of "unittest.h" looks like this (the complete program is
also
attached as "test.cpp"):

    #include "unittest.h"

    struct Account
    {
        Account();
        void deposit(double amount);
        double balance() const;

        ...
    };

    struct AccountTest
    {
        Account account; // a very simple fixture
    
        void testInit()
        {
            should(account.balance() == 0.0);
        }
    
        void testDeposit()
        {
            account.deposit(5.5);
            should(account.balance() == 5.5);
            account.deposit(100.0);
            should(account.balance() == 110.0); // force a test failure
        }

        ... // more tests
    };

    struct AccountTestSuite
    : public TestSuite
    {
        AccountTestSuite()
        : TestSuite("AccountTestSuite")
        {
            add( testCase(&AccountTest::testInit));
            add( testCase(&AccountTest::testDeposit));
        }
    };

    int main()
    {
        AccountTestSuite test;
        int failed = test.run();
        std::cout << test.report() << std::endl;
        return (failed != 0);
    }

Typical output would look like this:

    Failure in AccountTest::testDeposit() - assertion failed:
    "account.balance() == 110.0" (test.cpp:38)

    1 of 2 tests failed in TestSuite AccountTestSuite

Any interest in this tool?
Regards
Ulli

-- 
 ________________________________________________________________
|                                                                |
| Ullrich Koethe  Universität Hamburg / University of Hamburg    |
|                 FB Informatik / Dept. of Computer Science      |
|                 AB Kognitive Systeme / Cognitive Systems Group |
|                                                                |
| Phone: +49 (0)40 42883-2573                Vogt-Koelln-Str. 30 |
| Fax:   +49 (0)40 42883-2572                D - 22527 Hamburg   |
| Email: u.koethe_at_[hidden]               Germany             |
|        koethe_at_[hidden]                        |
| WWW:   http://kogs-www.informatik.uni-hamburg.de/~koethe/      |
|________________________________________________________________|

/**************************************************************/
/* */
/* Copyright U. Koethe */
/* Fraunhoferinstitut fuer Graphische Datenverarbeitung */
/* Rostock, Germany */
/* Wed Jun 24 15:11:45 MET 1998 */
/* */
/**************************************************************/
 
#ifndef UNITTEST_HXX
#define UNITTEST_HXX

#include <stdio.h>
#include <list>
#include <string>
#include <stdexcept>

#ifdef _MSC_VER

#include <wtypes.h>
#include <winbase.h>
#include <excpt.h>

#elif defined(__unix)

#include <signal.h>
#include <setjmp.h>

#else

#define CANT_CATCH_SIGNALS

#endif

#define testCase(function) createTestCase(function, #function "()")
#define testSuite(testsuite) ( new testsuite )

#define checkpoint(message) \
    { \
        char buf[1000]; \
        sprintf(buf, "%s (" __FILE__ ":%d)", (message), __LINE__); \
        testCheckpoint() = buf; \
    }

#define should(p) \
    {\
        checkpoint(#p) \
        if(p); else\
        { \
            char buf[1000]; \
            sprintf(buf, "\"" #p "\" (" __FILE__ ":%d)", __LINE__); \
            throw UnitTestFailed(buf); \
        } \
    }

#define shouldMsg(p, message) \
    {\
        checkpoint(message) \
        if(p); else \
        { \
            char buf[1000]; \
            sprintf(buf, "%s (" __FILE__ ":%d)", (message), __LINE__); \
            throw UnitTestFailed(buf); \
        } \
    }

#define failTest(message) shouldMsg(false, message)

struct UnitTestFailed
{
    UnitTestFailed(char * message)
    : what_(message)
    {}
    
    virtual const char * what() const
    {
        return what_.c_str();
    }
    
    std::string what_;
};

inline std::string & testCheckpoint()
{
    static std::string testCheckpoint_;
    return testCheckpoint_;
}

class TestCase
{
  public:

    TestCase(char const * name = "Unnamed")
    : name_(name)
    {}

    virtual ~TestCase() {}
    
    virtual int run() = 0;
    virtual void init() {}
    virtual void destroy() {}
    virtual bool hasCheckpoint() const
    {
        return testCheckpoint().size() > 0;
    }
    
    virtual std::string getCheckpointMessage() const
    {
        return testCheckpoint();
    }

    virtual char const * name() { return name_.c_str(); }
    virtual void setName(char const * name) { name_ = name; }
    virtual int size() const { return 1; }
    virtual std::string report() { return std::string(""); }

    std::string name_;
    
};

class TestSuite;

static int doRun(TestSuite *, TestCase *);

class TestSuite
: public TestCase
{
  public:
    TestSuite(char const * name = "TopLevel")
    : TestCase(name),
      size_(0)
    {}
    
    virtual ~TestSuite()
    {
        std::list<TestCase *>::iterator i = testcases_.begin();
        for(; i != testcases_.end(); ++i) delete (*i);
    }
    
    virtual void add(TestCase * t)
    {
        testcases_.push_back(t);
        size_ += t->size();
    }
   
    virtual int run()
    {
        int failed = 0;
        
        std::list<TestCase *>::iterator i = testcases_.begin();
        for(; i != testcases_.end(); ++i)
        {
            failed += doRun(this, *i);
            report_ += (*i)->report();
        }
        
        char buf[100];
        if(failed)
        {
            sprintf(buf, "%d of %d", failed, size());
            report_ += std::string("\n") + buf +
                       " tests failed in TestSuite " + name() + "\n";
        }
        else
        {
            sprintf(buf, "%d", size());
            report_ += std::string("\nAll (") + buf +
                       ") tests passed in TestSuite " + name() + "\n";
        }

        return failed;
    }
    
    virtual void handleError(const char * where, TestCase * t)
    {
        try
        {
            throw;
        }
        catch(UnitTestFailed & e)
        {
            report_ += std::string("\nFailure in ") + where + t->name() +
                         " - assertion failed: " + e.what() + "\n";
        }
        catch(std::exception & e)
        {
            report_ += std::string("\nFailure in ") + where + t->name() +
                         " - unexpected exception: " + e.what() + "\n";

            if(t->hasCheckpoint())
            {
                report_ += "Last checkpoint: " +
                                t->getCheckpointMessage() + "\n";
            }
        }
    }
    
    virtual int tryInit(TestCase * t)
    {
        try
        {
            t->init();
        }
        catch(...)
        {
            handleError("initialization of ", t);
            return 1;
        }

        return 0;
    }
    
    virtual int tryRun(TestCase * t)
    {
        int res = 0;
        
        try
        {
            res = t->run();
        }
        catch(...)
        {
            handleError("", t);
            return 1;
        }

        return res;
    }
    
    virtual int tryDestroy(TestCase * t)
    {
        try
        {
            t->destroy();
        }
        catch(...)
        {
            handleError("destruction of ", t);
            return 1;
        }

        return 0;
    }
    
    virtual int size() const { return size_; }
    virtual std::string report() { return report_; }
  
    std::list<TestCase *> testcases_;
    int size_;
    std::string report_;
};

static void handleUnrecognizedException(const char * where, TestSuite * ts, TestCase * tc)
{
    ts->report_ += std::string("\nFailure in ") + where + tc->name() +
                               " - unrecognized exception\n";

    if(tc->hasCheckpoint())
    {
        ts->report_ += "Last checkpoint: " +
                        tc->getCheckpointMessage() + "\n";
    }
}

#ifndef CANT_CATCH_SIGNALS

#ifdef _MSC_VER

static void handleSignal(const char * where, TestSuite * ts, TestCase * tc)
{
    ts->report_ += std::string("\nFailure in ") + where + tc->name();

    switch (GetExceptionCode())
    {
        case EXCEPTION_ACCESS_VIOLATION:
            ts->report_ += " - memory access violation\n";
            break;
        case EXCEPTION_INT_DIVIDE_BY_ZERO:
            ts->report_ += " - integer divide by zero\n";
            break;
        default:
            ts->report_ += " - unrecognized exception or signal\n";
    }

    if(tc->hasCheckpoint())
    {
        ts->report_ += "Last checkpoint: " +
                        tc->getCheckpointMessage() + "\n";
    }
}
   
static int doRun(TestSuite * ts, TestCase * tc)
{
    int current_failed = 0;
    
    __try
    {
        current_failed += ts->tryInit(tc);
    }
    __except (true)
    {
        handleSignal("initialization of ", ts, tc);
        current_failed++;
    }
   
    __try
    {
        if(current_failed == 0) current_failed += ts->tryRun(tc);
    }
    __except (true)
    {
        handleSignal("", ts, tc);
        current_failed++;
    }
    
    __try
    {
        if(current_failed == 0) current_failed += ts->tryDestroy(tc);
    }
    __except (true)
    {
        handleSignal("destruction of ", ts, tc);
        current_failed++;
    }

    return current_failed;
}

#elif defined(__unix)

inline jmp_buf & unitTestJumpBuffer()
{
    static jmp_buf unitTestJumpBuffer_;
    return unitTestJumpBuffer_;
}

static void unitTestSignalHandler(int sig, int code, struct sigcontext * c)
{
    longjmp(unitTestJumpBuffer(), sig);
}

static void handleSignal(int sigtype, const char * where, TestSuite * ts, TestCase * tc)
{
    ts->report_ += std::string("\nFailure in ") + where + tc->name();

    switch(sigtype)
    {
        case SIGTRAP:
            ts->report_ += " - SIGTRAP (perhaps integer divide by zero)\n";
            break;
        case SIGFPE:
            ts->report_ += " - SIGFPE (arithmetic exception)\n";
            break;
        case SIGSEGV:
        case SIGBUS:
            ts->report_ += " - memory access violation\n";
            break;
        default:
            ts->report_ += " - unrecognized signal\n";
    }

    if(tc->hasCheckpoint())
    {
        ts->report_ += "Last checkpoint: " +
                        tc->getCheckpointMessage() + "\n";
    }
}
   
static int doRun(TestSuite * ts, TestCase * tc)
{
    int current_failed = 0;
    int sigtype;

    sigset(SIGFPE, (SIG_PF)&unitTestSignalHandler);
    sigset(SIGTRAP, (SIG_PF)&unitTestSignalHandler);
    sigset(SIGSEGV, (SIG_PF)&unitTestSignalHandler);
    sigset(SIGBUS, (SIG_PF)&unitTestSignalHandler);

    try
    {
        sigtype = setjmp(unitTestJumpBuffer());
        if(sigtype == 0)
        {
           current_failed += ts->tryInit(tc);
        }
        else
        {
            handleSignal(sigtype, "initialization of ", ts, tc);
            ++current_failed;
        }
    }
    catch(...)
    {
        handleUnrecognizedException("initialization of ", ts, tc);
        ++current_failed;
    }
    
    if(current_failed) return current_failed;
   
    try
    {
        sigtype = setjmp(unitTestJumpBuffer());
        if(sigtype == 0)
        {
           current_failed += ts->tryRun(tc);
        }
        else
        {
            handleSignal(sigtype, "", ts, tc);
            ++current_failed;
        }
    }
    catch(...)
    {
        handleUnrecognizedException("", ts, tc);
        ++current_failed;
    }
    
    if(current_failed) return current_failed;
   
    try
    {
        sigtype = setjmp(unitTestJumpBuffer());
        if(sigtype == 0)
        {
           current_failed += ts->tryDestroy(tc);
        }
        else
        {
            handleSignal(sigtype, "destruction of ", ts, tc);
            ++current_failed;
        }
    }
    catch(...)
    {
        handleUnrecognizedException("destruction of ", ts, tc);
        ++current_failed;
    }

    return current_failed;
}

#endif /* _MSC_VER || __unix */

#else /* CANT_CATCH_SIGNALS */

static int doRun(TestSuite * ts, TestCase * tc)
{
    int current_failed = 0;

    try
    {
        current_failed += ts->tryInit(tc);
    }
    catch(...)
    {
        handleUnrecognizedException("initialization of ", ts, tc);
        ++current_failed;
    }
    
    if(current_failed) return current_failed;
   
    try
    {
        current_failed += ts->tryRun(tc);
    }
    catch(...)
    {
        handleUnrecognizedException("", ts, tc);
        ++current_failed;
    }
    
    if(current_failed) return current_failed;
   
    try
    {
        current_failed += ts->tryDestroy(tc);
    }
    catch(...)
    {
        handleUnrecognizedException("destruction of ", ts, tc);
        ++current_failed;
    }

    return current_failed;
}

#endif /* CANT_CATCH_SIGNALS */

template <class TESTCASE>
class ClassTestCase
: public TestCase
{
  public:
    
    ClassTestCase(void (TESTCASE::*fct)(), char const * name)
    : TestCase(name),
      fct_(fct),
      testcase_(0)
    {}
    
    virtual ~ClassTestCase()
    {
        delete testcase_;
    }
    
    virtual void init()
    {
        if(testcase_ != 0)
        {
            shouldMsg(0, "Attempt to run test case which failed "
                         "to clean up after previous run.");
        }
        
        testCheckpoint() = "";
        testcase_ = new TESTCASE;
    }
    
    virtual int run()
    {
        testCheckpoint() = "";
        if(testcase_ != 0) (testcase_->*fct_)();
        
        return 0;
    }
    
    virtual void destroy()
    {
        testCheckpoint() = "";
        TESTCASE * toDelete = testcase_;
        testcase_ = 0;
        delete toDelete;
    }
    
    void (TESTCASE::*fct_)();
    TESTCASE * testcase_;
};
    
class FunctionTestCase
: public TestCase
{
  public:
    
    FunctionTestCase(void (*fct)(), char const * name)
    : TestCase(name),
      fct_(fct)
    {}
    
    virtual int run()
    {
        testCheckpoint() = "";
        (*fct_)();
        
        return 0;
    }
    
    void (*fct_)();
};
    
template <class TESTCASE>
inline
TestCase *
createTestCase(void (TESTCASE::*fct)(), char const * name)
{
    if(*name == '&') ++name;
    return new ClassTestCase<TESTCASE>(fct, name);
};

inline
TestCase *
createTestCase(void (*fct)(), char const * name)
{
    if(*name == '&') ++name;
    return new FunctionTestCase(fct, name);
};

#endif /* UNITTEST_HXX */


#include <iostream>
#include <stdexcept>
#include "unittest.h"

struct Account
{
    Account()
    : m_amount(0.0)
    {}
    
    void deposit(double amount) { m_amount += amount; }
    void withdraw(double amount)
    {
        if(amount > m_amount)
        {
            throw std::runtime_error("You don't have that much money!");
        }
        m_amount -= amount;
    }
    double balance() const { return m_amount; }
    
    
    double m_amount;
};

struct AccountTest
{
    Account account; // a very simple fixture
    
    void testInit()
    {
        should(account.balance() == 0.0);
    }
    
    void testDeposit()
    {
        account.deposit(5.5);
        should(account.balance() == 5.5);
        account.deposit(100.0);
        should(account.balance() == 105.5);
    }
    
    void testWithdraw()
    {
        account.deposit(5.5);
        account.withdraw(2.5);
        should(account.balance() == 3.0);
        account.withdraw(3.0);
        should(account.balance() == 0.0);
    }
    
    void testFailure()
    {
        try
        {
            account.withdraw(2.5);
            failTest("no exception thrown!");
        }
        catch(std::runtime_error & ignore)
        {}
        // other errors will be reported, though
    }
    
    void forceMemoryAccessViolation()
    {
        int * i = 0;
        *i = 42;
    }
};

struct AccountTestSuite
: public TestSuite
{
    AccountTestSuite()
    : TestSuite("")
    {
        add( testCase(&AccountTest::testInit));
        add( testCase(&AccountTest::testDeposit));
        add( testCase(&AccountTest::testWithdraw));
        add( testCase(&AccountTest::testFailure));
        add( testCase(&AccountTest::forceMemoryAccessViolation));
    }
};

int main()
{
    AccountTestSuite test;

    int failed = test.run();

    std::cout << test.report() << std::endl;

    return (failed != 0);
}


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk