Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Testing C++ classes

4.72/5 (23 votes)
9 Jul 2011CPOL3 min read 40.5K  
Testing C++ classes with a pointer to member function.

Introduction

This is just an idea on how to write test classes for testing C++ classes. I might reinvent the wheel here (since I am not an expert on automated testing...) but an idea/code snippet may prove useful in certain simpler cases.

Background

Familiarity with pointer to C++ class functions will help. A good intro can be found here.

Support classes

The main idea is to accumulate pointers to members that modify an input value and apply them sequentially to an input instance to obtain the final value, and compare to an expected value.

The first class - called functor - keeps a pointer to a member function of class _F and has a name (the name is for display purposes only):

C++
template<typename _F> struct functor {
    _F& (_F:: *_pf)();
    std::string _name;

    functor(_F& (_F::*pf)(), const std::string& name) 
        : _pf(pf)
        , _name(name) {
        }
    functor(const functor& src) 
        : _pf(src._pf)
        , _name(src._name) {
        }
    virtual ~functor() {
        }
    _F& operator()(_F& arg) { 
        return (arg.*_pf)(); }
    const std::string& name() const {
        return _name; }
};

The point of interest here is the operator(), which will call the member function _pf on the object arg and return the object itself. Multiple calls of member functions on the same object somewhat resembles composing functions, where:

fn(...f2(f1(x)) ...)

will be in our case:

x.f1().f2(). ...

The next class, workflow, will encapsulate a sequence of such functors over a class _F and will call them in a single step, as described above:

C++
template<typename _F> 
struct workflow {
    std::vector<functor<_F>> _funcs;

    workflow() {
        }
    virtual ~workflow() {
        }
    workflow& operator+=(const functor<_F>& f) {
        _funcs.push_back(f);
        return *this; }
    _F& operator()(_F& arg) {
        std::vector<functor<_F>>::const_iterator itf;
        std::cout << "==> arg=" << arg << std::endl;
        for(itf = _funcs.begin(); itf != _funcs.end(); itf++) {
            std::cout << " --> " << itf->name().c_str() << " arg=" << arg << std::endl;
            (const_cast<functor<_F>&>(*itf))(arg);
            std::cout << " <-- " << itf->name().c_str() << " arg=" << arg << std::endl;
        }
        std::cout << "<== arg=" << arg << std::endl;
        return arg; }
};

Again, the same interesting function is the operator(), which will call the sequence of functors on the input object arg and will return the argument itself.

The final class is the tester_t template class, which accepts two arguments in its constructor, the input value and the expected value, has a variable-argument method test which encapsulates the entire testing, and offers strong/weak assertion support.

C++
template<typename _C>
struct tester_t {
    _C& _i;
    const _C& _e;

    tester_t(_C& i, const _C& e) 
        : _i(i)
        , _e(e) {
        }
    virtual ~tester_t() {
        }

    bool __cdecl 
    test(
        const char* name, 
        ...
    ) {
        typedef _C& (_C::* _PFMC)();
        typedef std::pair<_PFMC, std::string> PNFMC;

        workflow<_C> wf;

        va_list ap;
        va_start(ap, name);
        do {
            PNFMC* pfnmf = static_cast<PNFMC*>(va_arg(ap, PNFMC*));
            if(!pfnmf) {
                break; }
            else {
                wf += functor<_C>(pfnmf->first, pfnmf->second);
            }
        } while(true);
        va_end(ap);

        std::cout << "Running test:" << name << std::endl;
        wf(_i);

        return _i == _e;
    }

    void assert_succeeded() {
        assert(_i == _e); }
    void assert_fail() {
        assert(_i != _e); }
    void assert_ex(const std::string& msg, bool strong) {
        if(_i != _e) {
            std::cout << "assertion failure: expected " << _e 
                      << " but is " << _i 
                      << " FAILURE" << std::endl; if(strong) {
                assert(_i == _e); }
        }
        else {
            std::cout << "assertion passed : expected " 
              << _e << " and is " << _i 
              << " SUCCESS" << std::endl; }
    }
};
  1. The constructor has the first argument non-const since the functors applied to the object modifies it. The functors are of "A& A::function()" form because calling functors on an object modifies the object itself. (It is possible to have them also in "A A::function()" form or even non-member functions, "A func(A)" - whatever makes you comfortable.) The second argument is const because the "expected" value is used only for comparison.
  2. The test method has some pitfalls. Because it uses variable arguments, all arguments passed to the test call are pointers (I don't know if you can pass a const reference to va_arg). Also, the last argument of the test call is 0 to signal the end of the function calls list. For sure there are more prettier ways to signal the end to va_ calls, but for the moment I used a NULL pointer to stop argument extraction. The function call arguments are std::pair<pointer to member function, std::string>. The string contains the function name and is used, again, only for display purposes.

The pairs (function, name) are accumulated in the wf workflow variable, and the call wf(i); will invoke the workflow operator() and call all the accumulated functors.

The class to be tested

The test class is named integer in this example, which simply encapsulates an int variable and perform some very basic operations (the uninspired name dmult comes from double multiply since obviously double cannot be used).

C++
struct integer { 
    int _n;

    explicit integer(int n = 0) 
        : _n(n) {
        }
    virtual ~integer() {
        }

    bool operator==(int n) const { 
        return _n == n; }
    bool operator==(const integer& right) const {
        return _n == right._n; }
    bool operator!=(int n) const { 
        return _n != n; }
    bool operator!=(const integer& right) const { 
        return _n != right._n; }

    integer& nop() { 
        return *this; }
    integer& inc() { 
        ++_n; 
        return *this; }
    integer& dec() { 
        --_n; 
        return *this; }
    integer& dmult() { 
        _n *= 2; 
        return *this; }

    friend std::ostream& operator<<(std::ostream& o, const integer& i) {
        o << i._n; 
        return o; }
};

Nothing special here. Again, operator<< is added for display purposes.

Using the code

Finally, a simple test function will instantiate the input and the expected objects, create the tester test object by passing the previous variables, make the variable argument test call by passing the variadic sequence of pairs (function, name) to perform the workflow call, and finally perform basic assertions (as commented assert_succeeded) or, as in this example, a more relaxed assert_ex call which prints a message.

C++
void 
test_1() {
    integer input(2);
    integer expected(5);
    
    tester_t<integer> tester(input, expected);
    tester.test(
          "integer test (((2+1)*2)-1) ==> 5"
        , &std::make_pair<integer& (integer::*)(), std::string>(&integer::inc  , "inc  ")
        , &std::make_pair<integer& (integer::*)(), std::string>(&integer::dmult, "dmult")
        , &std::make_pair<integer& (integer::*)(), std::string>(&integer::dec  , "dec  ")
        , 0
    );
    //tester.assert_succeeded();
    tester.assert_ex("input 2 expected 5", false);
    std::cout << std::endl;

    return;
}

The image displays a sample result image produced by executing two sequential tests:

Image 1

History

  • Version 1.0 - 10 July 2011.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)