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 as 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 modifies an input value and apply them sequentially to an input instance to obtain the final value, and comparing to an expected value.  

The first class - called functor - keeps a pointer to a member function of class _F and have 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 resembles somewhat 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, have a variable-argument method test which encapsulate the entire testing, and offer 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; }
	}
};

1. Constructor have the first argument non-const since the functors applied to object modifies it. The functors are "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 have 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 test call is 0 to signal the end of 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 the 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, the 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 performs 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_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:

History

Version 1.0 - 10 July 2011.   

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架