一个简单地C++ Unit Test framework
一 使用
#ifndef _INTWRAPER_H #define _INTWRAPER_H class IntWraper { public: IntWraper(int n): m_nInteger(n){} void Value(int newValue){m_nInteger = newValue;} int Value( void )const{return m_nInteger;} IntWraper& operator += (IntWraper& rhs) { m_nInteger += rhs.Value(); return *this; } private: int m_nInteger; }; #endif //_INTWRAPER_H
现在要给它写一个unittest ,你只要做如下工作就可以了:
- 包含头文件: #include "FAssert.h"
- 定义一个测试类,在这个类中使用下列宏来申明test case:
DECLARE_AS_TESTER //申明一个类为tester,需要放在开头[必有]
DECLARE_SETUP //申明一个setup方法[可选]
DECLARE_TEARDOWN //申明一个teardown方法用于清理工作[可选]
DECLARE_TEST(testName) //申明一个test[至少有一个,否则没意义]
3. 实现定义的setup,teardown,和test方法;
4. 调用宏CALL_TEST注册这个unit test:
5. 在主程序中定义一个TestMain,然后调用它的run方法:
using namespace FTest;
TestMain myTest(std::cout);
myTest.run();
对IntWraper的完整的test 代码如下:
// SampleTest.cpp : Defines the entry point for the console application.
//
#include "IntWraper.h"
#include "FAssert.h"
#include "IntWraper.h"
class TestInt
{
DECLARE_AS_TESTER(TestInt)
DECLARE_SETUP
DECLARE_TEARDOWN
DECLARE_TEST(testValue)
DECLARE_TEST(testAdd)
private:
IntWraper *m_pInt;
};
void TestInt::setup( void )
{
m_pInt = new IntWraper(0);
}
void TestInt::tearDwon( void )
{
delete m_pInt;
}
void TestInt::testValue( void )
{
ASSERTEQUAL(0,m_pInt->Value());
}
void TestInt::testAdd( void )
{
IntWraper other(4);
*m_pInt += other;
ASSERTEQUAL(3 , m_pInt->Value());
}
CALL_TEST(TestInt)
#include <iostream>
int main(int argc, char* argv[])
{
using namespace FTest;
TestMain myTest(std::cout);
myTest.run();
return 0;
}
运行后你会得到输出结果:
从这个结果你可以看到一共运行了多少个test,成功多少,失败多少,如果失败了,还会给出失败的具体位置.
二 框架
下面这个结构图是从xunit的结构图演变而来的,只不过在这里的testcase是由一系列的IFuncObject组成。对上面给出的例子来说,setup,teardown,testValue和testAdd都对应一个IFuncObject.这些IFuncObject只是对保存了setup,teardown,和test函数这些成员函数指针而已。在TestCase的run方法被调用的时候,按照下列顺序调用这些IFuncObject的Do方法:
setup
test1
...
teardown
最后在内存中会形成如下图所示的一棵树。
这棵树有点特殊,因为它只有三层,最顶层的是一个suite,第二层是一些testcase,它们与程序员写的tester是一一对应的。最底层的是一些IFuncObject对象,它们对应与tester中的setup,teardown和具体的测试函数。由于对IFuncObject应用了NullObject模式,所以无论是否定义setup和teardown,每一个testcase,都有一个setup和一个teardown的IFuncObject。
调用的过程基本上是一个广度优先的过程。
三.实现
实现其实是很简单地,这里只说一下注册原理
3.1 注册
注册是通过TestObject来完成的,TestObject的申明如下:
template<typename Tester, void (Tester::*RealTest) ( void )>
class TestObject:public IFuncObject
它从IFuncObject继承下来的,接收两个模板参数,一个是tester,另一个是一个成员函数指针。一句
DECLARE_TEST(testValue)
会定义一个TestObject对象,这样在tester被创建的时候,就会自动将这个函数指针注册到tester的对应的TestCase之中。
3.2 跟踪测试用例
本来是想用异常来跟踪测试用例的运行状态的,可是发现这样就变得复杂啦。所以这个实现没有用,用了最简单地方式,调用类的全局函数来实现。
3.3 源码
源码只包含两个头文件FTest.h和FAssert.h
/*!
* Copyright (c) 2010 FengGe(CN), Inc
* All rights reserved
* This program is UNPUBLISHED PROPRIETARY property of FengGe(CN).
* Only for internal distribution.
*
* @file: FTest.h
*
* @brief: define the framework for unit test
*
* @author: li_shugan@126.com
*
* @version: 1.0
*
* @date: 2010-12-25
*/
#ifndef _FTEST_H
#define _FTEST_H
#include <list>
#include <vector>
#include <algorithm>
#include <ostream>
namespace FTest
{
using std::list;
using std::vector;
using std::find;
using std::ostream;
class Test
{
public:
virtual ~Test(){}
virtual void run( void ) = 0;
};
class TestSuite:public Test
{
public:
static TestSuite* getRootSuite( void )
{
if (NULL == g_pRootSuite)
{
g_pRootSuite = new TestSuite();
}
return g_pRootSuite;
}
void add(Test *pTest)
{
m_listTests.push_back(pTest);
}
void remove(Test* pTest)
{
m_listTests.erase(find(m_listTests.begin(),m_listTests.end(),pTest));
}
virtual void run( void )
{
for_each(m_listTests.begin(),m_listTests.end(),RunFun);
}
private:
static void RunFun(Test* itFun)
{
itFun->run();
}
list<Test*> m_listTests;
static TestSuite* g_pRootSuite;
};
TestSuite *TestSuite::g_pRootSuite = NULL;
class IFuncObject
{
public:
virtual void Do( void ) {}
static IFuncObject NULLFucObject;
static IFuncObject* NullFunction( void ){return &NULLFucObject;}
};
IFuncObject IFuncObject::NULLFucObject = IFuncObject();
class TestCase:public Test
{
public:
TestCase():
m_funSetup(IFuncObject::NullFunction())
,m_funTeardown(IFuncObject::NullFunction())
{
}
virtual void run( void )
{
m_funSetup->Do();
for_each(m_vTestObjects.begin(),m_vTestObjects.end(),&RunFunc);
m_funTeardown->Do();
}
void setSetup(IFuncObject *pSetup){m_funSetup = pSetup;}
void setTearDown(IFuncObject *pTearDown){m_funTeardown = pTearDown;}
void addTestObject(IFuncObject *pObject){m_vTestObjects.push_back(pObject);}
private:
static void RunFunc(IFuncObject* itFun)
{
itFun->Do();
}
IFuncObject *m_funSetup;
IFuncObject *m_funTeardown;
vector<IFuncObject*> m_vTestObjects;
};
template<typename Tester, void (Tester::*RealTest) ( void )>
class TestObject:public IFuncObject
{
public:
TestObject( void ){ Tester::getCase().addTestObject(this);}
virtual void Do( void ){(Tester::g_pInstance->*RealTest)();}
};
template<typename Tester, void (Tester::*Setup) ( void )>
class SetupObject:public IFuncObject
{
public:
SetupObject( void ){ Tester::getCase().addTestObject(this);}
virtual void Do( void ){(Tester::g_pInstance->*Setup)();}
};
template<typename Tester, void (Tester::*TearDown) ( void )>
class TearDownObject:public IFuncObject
{
public:
TearDownObject( void ){ Tester::getCase().setTearDown(this);}
virtual void Do( void ){(Tester::g_pInstance->*TearDown)();}
};
template<typename Tester>
class TestCaller
{
public:
TestCaller()
{
Tester::g_pInstance = new Tester();
TestSuite::getRootSuite()->add(&Tester::getCase());
}
~TestCaller()
{
delete Tester::g_pInstance;
}
};
}
#define DECLARE_AS_TESTER(tester) typedef tester TestCaseType; \
static FTest::TestCase __testCase; \
public: \
static FTest::TestCase& getCase( void ){return __testCase;} \
static tester* g_pInstance;
#define DECLARE_TEST(testName) public: \
void testName( void ); \
FTest::TestObject<TestCaseType,&TestCaseType::testName> __test##testName; \
#define DECLARE_SETUP public: \
void setup( void ); \
FTest::SetupObject<TestCaseType,&TestCaseType::setup> __test##setup;
#define DECLARE_TEARDOWN public: \
void tearDwon( void ); \
FTest::TearDownObject<TestCaseType,&TestCaseType::tearDwon> __test##tearDwon;
#define CALL_TEST(tester) tester* tester::g_pInstance = NULL; \
FTest::TestCase tester::__testCase = FTest::TestCase(); \
FTest::TestCaller<tester> __##tester##callTester;
#endif //FTEST_H
/*!
* Copyright (c) 2010 FengGe(CN), Inc
* All rights reserved
* This program is UNPUBLISHED PROPRIETARY property of FengGe(CN).
* Only for internal distribution.
*
* @file: FAssert.h
*
* @brief: used for my test framework
*
* @author: li_shugan@126.com
*
* @version: 1.0
*
* @date: 2010-12-25
*/
#ifndef _FASSERT_H
#define _FASSERT_H
#include <ostream>
#include <string>
#include <sstream>
#include "FTest.h"
namespace FTest
{
using std::ostream;
using std::endl;
using std::string;
using std::stringstream;
class TestMain
{
public:
TestMain(ostream& rOstream):
m_pStream(&rOstream)
,m_nFailedCnt(0)
,m_nSuccCnt(0)
{
g_pInstance = this;
}
~TestMain( )
{
getSteam()<<"Total: "<<m_nFailedCnt + m_nSuccCnt << " Success: "<<m_nSuccCnt<<" Failed: "<<m_nFailedCnt<<endl;
g_pInstance = NULL;
}
void run()
{
TestSuite::getRootSuite()->run();
}
public:
static ostream& getSteam(){return *(g_pInstance->m_pStream);}
static void addFaildCnt() {++g_pInstance->m_nFailedCnt;}
static void addSuccCnt() {++g_pInstance->m_nSuccCnt;}
private:
ostream* m_pStream;
int m_nFailedCnt;
int m_nSuccCnt;
static TestMain* g_pInstance;
};
TestMain* TestMain::g_pInstance = NULL;
inline void Assert(bool bTrue,const string& strFile,int nLine)
{
if (!bTrue)
{
TestMain::getSteam()<<"Failed at "<<strFile<<"("<<nLine<<")"<<endl;
TestMain::addFaildCnt();
}
else
{
TestMain::addSuccCnt();
}
}
template <typename T1,typename T2>
void AssertEqual(const T1& lhs,const T2& rhs,const string& strFile,int nLine)
{
if (lhs != rhs)
{
TestMain::getSteam()<<"Failed at "<<strFile<<nLine<<": expected "<<lhs<<" but "<<rhs<<endl;
TestMain::addFaildCnt();
}
else
{
TestMain::addSuccCnt();
}
}
}
#define ASSERT(b) FTest::Assert(b,__FUNCTION__,__LINE__)
#define ASSERTFAIL(b) FTest::Assert(!b,__FUNCTION__,__LINE__)
#define ASSERTEQUAL(lhs,rhs) FTest::AssertEqual(lhs,rhs,__FUNCTION__,__LINE__)
#endif //FASSERT_H