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):
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:
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.
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; }
}
};
- 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. - 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).
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.
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_ex("input 2 expected 5", false);
std::cout << std::endl;
return;
}
The image displays a sample result image produced by executing two sequential tests:
History
- Version 1.0 - 10 July 2011.