个人编程中比较喜欢重构,重构能够提高自己的代码质量,使代码阅读起来也更清晰。但是重构有一个问题,就是如何保证重构后带代码实现的功能与重构前的一致,如果每次重构完成后,对此不闻不问,则会有极大的风险,如果每次重构后,都进行一边测试,则工作量会很巨大,最终可能是即使代码有重构的欲望,也会尽量克制住,不去重构。除非代码能够进行自动化测试。实际上进行测试的是接口,而不是所有代码,只要能够保持接口不变,自动化测试的工作量也没有想象中的巨大。其实我们在单元测试的时候,会测试各种异常情况,只不过,没有将这些测试写成测试代码罢了。
在Java中有JUnit,在C#中有NUnit,在C++中,笔者并不知道有哪些自动化测试工具(笔者的孤陋寡闻)。于是就产生了自己写一个自动化测试程序的想法。
自动化测试程序本质上应该是一个Command模式的应用案例,将多个Command对象保存起来,这些Command对象中存储着测试函数,在需要的时候,运行这些Command对象,并根据这些Command对象的执行结果判断测试是否通过。
首先就是定义Command对象。Command对象比较简单,定义如下:
typedef std::function<bool(TestInfo&)> TestFun;
这是一个返回值为布尔类型,输入参数为TestInfo引用的函数对象,如果返回值返回true表示测试通过,返回false表示测试未通过。
TestInfo也不复杂,它主要包含本次测试的一些信息,包括测试属性和测试结果等。测试属性有这个测试的名称,一些描述以及是否期望抛出异常等,测试结果就是测试是否成功。TestInfo代码如下:
/** * @brief 测试信息对象,保存测试信息及测试结果。 * */ class TestInfo { public: TestInfo() { level = 1; name = ""; subName = ""; isOK = false; isWantException = false; remark = ""; } public: int level; /**< 测试用例级别 */ std::string name; /**< 测试接口名称 */ std::string subName; /**< 测试接口名称具体描述 */ bool isOK; /**< 测试结果 */ bool isWantException; /**< 是否期望异常发生 */ std::string remark; /**< 备注信息 */ };
有了Command对象,还需要有存储及运行Command对象的地方,因此我添加了一个叫TestBaseEX的类,之所以叫Ex,是因为我以前实现过一个TestBase的类,后来在TestBase的基础上做了改进,名称编程了TestBaseEx。
在TestBaseEX中,将Command对象存储在一个vector中,并提供了一个OnTest方法来运行这些Command对象。
/** * @brief 测试基础类。 * */ class TestBaseEX { public: typedef std::function<bool(TestInfo&)> TestFun; /** * @brief 执行测试。 * @param[in] testShow 测试结果展示函数 * */ void OnTest(std::function<void(TestInfo&)> testShow) { for (auto it = m_Tests.begin(); it != m_Tests.end(); ++it) { TestInfo info; try { bool result = (*it)(info); if (info.isWantException) { info.isOK = false; } else { info.isOK = result; } } catch (...) { info.exception = "有异常"; if (info.isWantException) { info.isOK = true; } } testShow(info); } } public: std::vector<TestFun> m_Tests; };
TestBaseEX中主要是一个OnTest方法,该方法逻辑比较简单:
循环遍历Command对象并执行,如果期望抛出异常而没有抛异常,则测试部通过,否则根据返回值判断 是否测试通过。
传入的testShow函数对象负责对Command对象测试结果进行处理(一般是进行展示,如果通过显示为绿色,不通过,显示为红色)。
在创建Command对象的时候,信息越全越好,最好能将函数,参数什么的都包含进行,所以添加了一个宏,来创建Command对象。
/** * @brief 添加测试对象。 * */ #define TEST_INIT(info, sub) { ostringstream oss; oss<<"position:"<<__FILE__<<"-"<<__LINE__<<"-"<<__FUNCTION__<<endl; info.name = __FUNCTION__;/*oss.str();*/ } info.subName = sub; info.remark = ""; info.isOK = true; #define TESTFUN_INIT(name) m_Tests.push_back(std::bind(&name, this, std::tr1::placeholders::_1))
真正的测试类会继承自TestBaseEX,例如HiDB的测试类:
/** * @brief 数据库操作测试类。 * */ class HisDBTest: public TestBaseEX { public: HisDBTest(); ~HisDBTest(); private: /** * @brief 执行Open接口测试(连接字符串正确)。 * @param[in] info 测试数据对象 * @retval true:成功,false;失败 */ bool OnOpen(TestInfo& info); bool DropTable(TestInfo&); bool CreateTable(TestInfo&); bool AddRecorder(TestInfo&); bool AddRecorder2(TestInfo&); bool Scalar(TestInfo&); bool Scalar2(TestInfo&); bool Scalar3(TestInfo&); bool ReadRecorders(TestInfo&); bool ReadRecorders2(TestInfo&); bool ReadRecorders3(TestInfo&); bool DeleteRecorder(TestInfo&); bool OnClose(TestInfo&); bool OnClose_Repeat(TestInfo&); bool OnConnRelace2(TestInfo&); bool OnConnRelace3(TestInfo&); private: HiDB* m_DB; };
实现(此处和我上一篇的HiDB对应不起来,这是我很久以前的HiDB版本,要比现在的复杂很多):
using namespace std; HisDBTest::HisDBTest() { TESTFUN_INIT(HisDBTest::OnOpen); TESTFUN_INIT(HisDBTest::DropTable); TESTFUN_INIT(HisDBTest::CreateTable); TESTFUN_INIT(HisDBTest::AddRecorder); TESTFUN_INIT(HisDBTest::AddRecorder2); TESTFUN_INIT(HisDBTest::Scalar); TESTFUN_INIT(HisDBTest::Scalar2); TESTFUN_INIT(HisDBTest::Scalar3); TESTFUN_INIT(HisDBTest::ReadRecorders); TESTFUN_INIT(HisDBTest::ReadRecorders2); TESTFUN_INIT(HisDBTest::ReadRecorders3); TESTFUN_INIT(HisDBTest::DeleteRecorder); TESTFUN_INIT(HisDBTest::OnConnRelace2); TESTFUN_INIT(HisDBTest::OnConnRelace3); this->m_DB = new HiDB(HiDBType_MySQL, false); } HisDBTest::~HisDBTest() { if (this->m_DB) { this->m_DB->Close(); delete this->m_DB; this->m_DB = NULL; } } bool HisDBTest::OnOpen(TestInfo& info) { TEST_INIT(info, "打开数据库"); info.remark = "(请提供数据库:host=127.0.0.1;port=3306;" "dbname=test;user=root;pwd=root;charset=gbk;"; return this->m_DB->Open( "host=127.0.0.1;port=3306;dbname=test;user=root;pwd=root;charset=gbk;" ); } bool HisDBTest::DropTable(TestInfo& info) { TEST_INIT(info, "ExecuteNoQuery 无参"); return this->m_DB->ExecuteNoQuery("drop table if exists table1;"); } bool HisDBTest::CreateTable(TestInfo& info) { TEST_INIT(info, "ExecuteNoQuery 无参"); return this->m_DB->ExecuteNoQuery( "create table table1(column1 varchar(6) not null," "column2 varchar(40) not null," "column3 int not null default 1," "column4 int, " "column5 timestamp not null default CURRENT_TIMESTAMP," "column6 varchar(512),primary key (column1));"); } bool HisDBTest::AddRecorder(TestInfo& info) { TEST_INIT(info, "ExecuteNoQuery C语言方式(printf)"); return this->m_DB->ExecuteNoQuery( "INSERT INTO table1(Column1,Column2,Column3,Column4,Column6) " "VALUES('%s', '%s', %d, NULL, '%s')", "mytest", "my second test recorder", 80, "this test create by xuminrong"); } bool HisDBTest::AddRecorder2(TestInfo& info) { TEST_INIT(info, "Create方法,自动组成SQL语句"); vector<HiDBParamer> paramers; HiDBParamer paramer1("column1", "hitest"); paramers.push_back(paramer1); HiDBParamer paramer2("column2", "this is a test by xmr"); paramers.push_back(paramer2); HiDBParamer paramer3(HiDBDataType_Short, "column3", "59"); paramers.push_back(paramer3); HiDBParamer paramer4(HiDBDataType_Short, "column4", "59"); paramer4.m_IsNull = true; paramers.push_back(paramer4); HiDBParamer paramer6("column6", "this is a test");// = {0}; paramers.push_back(paramer6); return this->m_DB->Create("table1", paramers); } bool HisDBTest::Scalar(TestInfo& info) { TEST_INIT(info, "ExecuteScalar 使用参数数组,以HiDBRetVal作为返回值"); vector<HiDBParamer> paramers; HiDBParamer paramer1("column1", "hitest"); paramers.push_back(paramer1); HiDBParamer paramer2(HiDBDataType_Short, "column3", "59"); paramers.push_back(paramer2); HiDBRetVal val; try { if (!this->m_DB->ExecuteScalar("SELECT column6 FROM table1 WHERE ? AND ?",paramers, &val)) { return false; } } catch (HiDBException& e) { info.remark = "产生HiDBException异常:" + e.m_descript + e.m_position; } if (::strcmp(val.m_Value.c_str(), "this is a test") != 0) { return false; } return true; } bool HisDBTest::Scalar2(TestInfo& info) { TEST_INIT(info, "ExecuteScalar C语言方式(printf),以string作为返回值"); string val; try { return this->m_DB->ExecuteScalar( "SELECT column4 FROM table1 WHERE column1='%s' AND column3=%d", &val, "hitest", 59); } catch (HiDBException& e) { info.remark = "产生HiDBException异常:" + e.m_descript + e.m_position; return false; } } bool HisDBTest::Scalar3(TestInfo& info) { TEST_INIT(info, "ExecuteScalar C语言方式(printf),返回空值"); HiDBRetVal val; try { if (!this->m_DB->ExecuteScalar( "SELECT column4 FROM table1 WHERE column1='%s' AND column3=%d", &val, "hitest", 59)) { return false; } } catch (HiDBException& e) { info.remark = "产生HiDBException异常:" + e.m_descript + e.m_position; return false; } if (val.m_IsNull) { return true; } else { return false; } } bool HisDBTest::ReadRecorders(TestInfo& info) { TEST_INIT(info, "ExecuteQuery 使用参数数组"); vector<HiDBParamer> paramers; HiDBParamer paramer1("column1", "hitest"); paramers.push_back(paramer1); HiDBParamer paramer2( "column1", "mytest"); paramers.push_back(paramer2); std::shared_ptr<HiDBTable> table = this->m_DB->ExecuteQuery( "SELECT column1,column2 FROM table1 WHERE ? OR ? ORDER BY column1", paramers); if (table == NULL) { return false; } if (table->size() != 2) { return false; } ostringstream oss; for (auto it = table->begin(); it != table->end(); ++it) { for(auto item = (*it).begin(); item != (*it).end(); ++item) { //oss<<"field:"<<item->second.m_Field.c_str()<<",value:"<<item->second.m_Value.c_str()<<" "; oss<<item->second.ToSrting()<<" "; } } info.remark.append(oss.str().c_str()); return true; } bool HisDBTest::ReadRecorders2(TestInfo& info) { TEST_INIT(info, "ExecuteQuery C语言方式(printf)"); std::shared_ptr<HiDBTable> table = this->m_DB->ExecuteQuery( "SELECT column1,column2 FROM table1 WHERE column1='%s' OR column1='%s'", "hitest", "mytest"); if (table == NULL) { return false; } if (table->size() != 2) { return true; } ostringstream oss; for (auto it = table->begin(); it != table->end(); ++it) { for(auto item = (*it).begin(); item != (*it).end(); ++item) { //oss<<"field:"<<item->second.m_Field.c_str()<<",value:"<<item->second.m_Value.c_str()<<" "; oss<<item->second.ToSrting()<<" "; } } info.remark.append(oss.str().c_str()); return true; } bool HisDBTest::ReadRecorders3(TestInfo& info) { TEST_INIT(info, "生成SQL语句测试"); vector<HiDBParamer> list; HiDBParamer parm1("nCameraNo", 12345); list.push_back(parm1); HiDBParamer parm2(HiDBDataType_Time, "dtTime", "2012-08-06 16:44:32"); parm2.m_Oper = HiOper_GreaterEqual; list.push_back(parm2); HiDBParamer parm3(HiDBDataType_Time, "dtTime", "2012-08-07 16:44:32"); parm3.m_Oper = HiOper_LessEqual; list.push_back(parm3); HiDBParamer parm4("nEPType", 3); list.push_back(parm4); HiDBParamer parm6( "nFoward", 5); list.push_back(parm6); ostringstream oss; oss<<"SELECT nCameraNo,nCarNoType,cCarNo,dtTime,cAddress," "cRouteChannel,nFoward,nEPType,nCaptureType,cAction," "nTotalTime,nColor FROM Illegals"; int count = (int)list.size(); if (count > 0) { oss<< " WHERE "; } for (int i = 0; i < count; i++) { oss<<" ? "; if (i != count - 1) { oss<<" AND "; } } oss<<" ORDER BY nNo LIMIT "<<3<<" "<<50<<endl; try { string sql = oss.str(); std::shared_ptr<HiDBTable> table = this->m_DB->ExecuteQuery(sql.c_str(),list); if (table == NULL) { return true; } return false; } catch (HiDBException& ex) { info.remark = ex.m_sql; return true; } catch (...) { return false; } } bool HisDBTest::DeleteRecorder(TestInfo& info) { TEST_INIT(info, "Delete 使用参数数组"); vector<HiDBParamer> paramers; HiDBParamer paramer1("column1", "hitest"); paramers.push_back(paramer1); HiDBParamer paramer2( HiDBDataType_Short, "column3", "59"); paramers.push_back(paramer2); return this->m_DB->Delete("table1", paramers); } static string getConn(string ip, TestInfo& info); bool HisDBTest::OnConnRelace2(TestInfo& info) { TEST_INIT(info, "替换数据库IP"); return getConn("127.0.15.43", info)=="host=127.0.0.1;port=3306;" "dbname=test;user=root;pwd=root;charset=gbk;"; } bool HisDBTest::OnConnRelace3(TestInfo& info) { TEST_INIT(info, "替换数据库IP"); return getConn("127.1.5.1", info)=="host=127.1.5.1;port=3306;" "dbname=test;user=root;pwd=root;charset=gbk;"; } static string getConn(string m_CenterIP, TestInfo& info) { string conn = "host=127.0.0.1;port=3306;" "dbname=test;user=root;pwd=root;charset=gbk;"; if (!m_CenterIP.empty()) { string::size_type pos1 = conn.find_first_of('='); string::size_type pos2 = conn.find_first_of(';', pos1); //取数据库IP string ip = conn.substr(pos1 + 1, pos2 - pos1 - 1); string::size_type pos_connect = ip.find_first_of('.', ip.find_first_of('.') + 1); string::size_type pos_center = m_CenterIP.find_first_of('.', m_CenterIP.find_first_of('.') + 1); //比较IP前两段是否一样 if (ip.substr(0, pos_connect) != m_CenterIP.substr(0, pos_center)) { conn.replace(pos1 + 1, ip.size(), m_CenterIP); } } return conn; }
源代码下载地址:http://download.csdn.net/detail/xumingxsh/7791923
本文转自我的另一个博客“逆水行船”