首页 > 代码库 > 学习实践:使用模式,原则实现一个C++自动化测试程序

学习实践:使用模式,原则实现一个C++自动化测试程序

个人编程中比较喜欢重构,重构能够提高自己的代码质量,使代码阅读起来也更清晰。但是重构有一个问题,就是如何保证重构后带代码实现的功能与重构前的一致,如果每次重构完成后,对此不闻不问,则会有极大的风险,如果每次重构后,都进行一边测试,则工作量会很巨大,最终可能是即使代码有重构的欲望,也会尽量克制住,不去重构。除非代码能够进行自动化测试。实际上进行测试的是接口,而不是所有代码,只要能够保持接口不变,自动化测试的工作量也没有想象中的巨大。其实我们在单元测试的时候,会测试各种异常情况,只不过,没有将这些测试写成测试代码罢了。

在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()<<"\r\n";            oss<<item->second.ToSrting()<<"\n";        }    }    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()<<"\r\n";            oss<<item->second.ToSrting()<<"\n";        }    }    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

 

本文转自我的另一个博客“逆水行船”