您的位置:首页 > 编程语言 > C语言/C++

轻松编写c++单元测试

2013-08-22 15:12 225 查看
单元测试概述

单元测试概述测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量(Quality),为所编写的功能代码编写适量的单元测试是十分必要的。

单元测试(UnitTest,模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确,通过编写单元测试可以在编码阶段发现程序编码错误,甚至是程序设计错误。

单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在回归测试的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。

对于单元测试框架,目前最为大家所熟知的是JUnit及其针对各语言的衍生产品,C++语言所对应的JUnit系单元测试框架就是CppUnit。但是由于CppUnit的设计严格继承自JUnit,而没有充分考虑C++与Java固有的差异(主要是由于C++没有反射机制,而这是JUnit设计的基础),在C++中使用CppUnit进行单元测试显得十分繁琐,这一定程度上制约了CppUnit的普及。笔者在这里要跟大家介绍的是一套由google发布的开源单元测试框架(Testing
Framework):googletest。

应用googletest编写单元测试代码

应用
googletest编写单元测试代码googletest是由Google公司发布,且遵循NewBSDLicense(可用作商业用途)的开源项目,并且googletest可以支持绝大多数大家所熟知的平台。与CppUnit不同的是:googletest可以自动记录下所有定义好的测试,不需要用户通过列举来指明哪些测试需要运行。

定义单元测试

在应用googletest编写单元测试时,使用TEST()宏来声明测试函数。如:

清单1.用TEST()宏声明测试函数

TEST(GlobalConfigurationTest,configurationDataTest)
TEST(GlobalConfigurationTest,noConfigureFileTest)

分别针对同一程序单元GlobalConfiguration声明了两个不同的测试(Test)函数,以分别对配置数据进行检查(configurationDataTest),以及测试没有配置文件的特殊情况(noConfigureFileTest)。

实现单元测试

针对同一程序单元设计出不同的测试场景后(即划分出不同的Test后),开发者就可以编写单元测试分别实现这些测试场景了。

在googletest中实现单元测试,可通过ASSERT_*和EXPECT_*断言来对程序运行结果进行检查。ASSERT_*版本的断言失败时会产生致命失败,并结束当前函数;EXPECT_*版本的断言失败时产生非致命失败,但不会中止当前函数。因此,ASSERT_*常常被用于后续测试逻辑强制依赖的处理结果的断言,如创建对象后检查指针是否为空,若为空,则后续对象方法调用会失败;而EXPECT_*则用于即使失败也不会影响后续测试逻辑的处理结果的断言,如某个方法返回结果的多个属性的检查。

googletest中定义了如下的断言:

表1:googletest定义的断言(Assert)

基本断言二进制比较字符串比较
ASSERT_TRUE(condition);

EXPECT_TRUE(condition);

condition为真

ASSERT_FALSE(condition);

EXPECT_FALSE(condition);

condition为假
ASSERT_EQ(expected,actual);

EXPECT_EQ(expected,actual);

expected==actual

ASSERT_NE(val1,val2);

EXPECT_NE(val1,val2);

val1!=val2

ASSERT_LT(val1,val2);

EXPECT_LT(val1,val2);

val1<val2

ASSERT_LE(val1,val2);

EXPECT_LE(val1,val2);

val1<=val2

ASSERT_GT(val1,val2);

EXPECT_GT(val1,val2);

val1>val2

ASSERT_GE(val1,val2);

EXPECT_GE(val1,val2);

val1>=val2
ASSERT_STREQ(expected_str,actual_str);

EXPECT_STREQ(expected_str,actual_str);

两个C字符串有相同的内容

ASSERT_STRNE(str1,str2);

EXPECT_STRNE(str1,str2);

两个C字符串有不同的内容

ASSERT_STRCASEEQ(expected_str,actual_str);

EXPECT_STRCASEEQ(expected_str,actual_str);

两个C字符串有相同的内容,忽略大小写

ASSERT_STRCASENE(str1,str2);

EXPECT_STRCASENE(str1,str2);

两个C字符串有不同的内容,忽略大小写
下面的实例演示了上面部分断言的使用:

清单2.一个较完整的googletest单元测试实例

//Configure.h
#pragmaonce

#include<string>
#include<vector>

classConfigure
{
private:
std::vector<std::string>vItems;

public:
intaddItem(std::stringstr);

std::stringgetItem(intindex);

intgetSize();
};

//Configure.cpp
#include"Configure.h"

#include<algorithm>

/**
*@briefAddanitemtoconfigurationstore.Duplicateitemwillbeignored
*@paramstritemtobestored
*@returntheindexofaddedconfigurationitem
*/
intConfigure::addItem(std::stringstr)
{
std::vector<std::string>::const_iteratorvi=std::find(vItems.begin(),vItems.end(),str);
if(vi!=vItems.end())
returnvi-vItems.begin();

vItems.push_back(str);
returnvItems.size()-1;
}

/**
*@briefReturntheconfigureitematspecifiedindex.
*Iftheindexisoutofrange,""willbereturned
*@paramindextheindexofitem
*@returntheitematspecifiedindex
*/
std::stringConfigure::getItem(intindex)
{
if(index>=vItems.size())
return"";
else
returnvItems.at(index);
}

///Retrievetheinformationabouthowmanyconfigurationitemswehavehad
intConfigure::getSize()
{
returnvItems.size();
}

//ConfigureTest.cpp
#include<gtest/gtest.h>

#include"Configure.h"

TEST(ConfigureTest,addItem)
{
//dosomeinitialization
Configure*pc=newConfigure();

//validatethepointerisnotnull
ASSERT_TRUE(pc!=NULL);

//callthemethodwewanttotest
pc->addItem("A");
pc->addItem("B");
pc->addItem("A");

//validatetheresultafteroperation
EXPECT_EQ(pc->getSize(),2);
EXPECT_STREQ(pc->getItem(0).c_str(),"A");
EXPECT_STREQ(pc->getItem(1).c_str(),"B");
EXPECT_STREQ(pc->getItem(10).c_str(),"");

deletepc;
}

运行单元测试

在实现完单元测试的测试逻辑后,可以通过RUN_ALL_TESTS()来运行它们,如果所有测试成功,该函数返回0,否则会返回1。RUN_ALL_TESTS()会运行你链接到的所有测试――它们可以来自不同的测试案例,甚至是来自不同的文件。

因此,运行googletest编写的单元测试的一种比较简单可行的方法是:

为每一个被测试的class分别创建一个测试文件,并在该文件中编写针对这一class的单元测试;

编写一个Main.cpp文件,并在其中包含以下代码,以运行所有单元测试:

清单3.初始化googletest并运行所有测试

#include<gtest/gtest.h>

intmain(intargc,char**argv){
testing::InitGoogleTest(&argc,argv);

//RunsalltestsusingGoogleTest.
returnRUN_ALL_TESTS();
}

最后,将所有测试代码及Main.cpp编译并链接到目标程序中。

此外,在运行可执行目标程序时,可以使用--gtest_filter来指定要执行的测试用例,如:

./foo_test没有指定
filter
,运行所有测试;


./foo_test--gtest_filter=*指定
filter
*
,运行所有测试;


./foo_test--gtest_filter=FooTest.*运行测试用例
FooTest
的所有测试;


./foo_test--gtest_filter=*Null*:*Constructor*运行所有全名(即测试用例名+“.”+测试名,如GlobalConfigurationTest.noConfigureFileTest
含有
"Null"
"Constructor"
的测试;


./foo_test--gtest_filter=FooTest.*-FooTest.Bar运行测试用例
FooTest
的所有测试,但不包括
FooTest.Bar


这一特性在包含大量测试用例的项目中会十分有用。

应用googlemock编写MockObjects

很多C++程序员对于MockObjects(模拟对象)可能比较陌生,模拟对象主要用于模拟整个应用程序的一部分。在单元测试用例编写过程中,常常需要编写模拟对象来隔离被测试单元的“下游”或“上游”程序逻辑或环境,从而达到对需要测试的部分进行隔离测试的目的。

例如,要对一个使用数据库的对象进行单元测试,安装、配置、启动数据库、运行测试,然后再卸装数据库的方式,不但很麻烦,过于耗时,而且容易由于环境因素造成测试失败,达不到单元测试的目的。模仿对象提供了解决这一问题的方法:模仿对象符合实际对象的接口,但只包含用来“欺骗”测试对象并跟踪其行为的必要代码。因此,其实现往往比实际实现类简单很多。

为了配合单元测试中对MockingFramework的需要,Google开发并于2008年底开放了:googlemock。与googletest一样,googlemock也是遵循NewBSDLicense(可用作商业用途)的开源项目,并且googlemock也可以支持绝大多数大家所熟知的平台。

注1:在Windows平台上编译googlemock

对于Linux平台开发者而言,编译googlemock可能不会遇到什么麻烦;但是对于Windows平台的开发者,由于VisualStudio还没有提供tuple(C++0xTR1中新增的数据类型)的实现,编译googlemock需要为其指定一个tuple类型的实现。著名的开源C++程序库boost已经提供了tr1的实现,因此,在Windows平台下可以使用boost来编译googlemock。为此,需要修改%GMOCK_DIR%/msvc/gmock_config.vsprops
,设定其中BoostDir到boost所在的目录,如:
<UserMacro
Name="BoostDir"
Value="$(BOOST_DIR)"
/>

其中BOOST_DIR是一个环境变量,其值为boost库解压后所在目录。

对于不希望在自己的开发环境上解包boost库的开发者,在googlemock的网站上还提供了一个从boost库中单独提取出来的tr1的实现,可将其下载后将解压目录下的boost目录拷贝到%GMOCK_DIR%下(这种情况下,请勿修改上面的配置项;建议对boost不甚了解的开发者采用后面这种方式)。

在应用googlemock来编写Mock类辅助单元测试时,需要:

编写一个MockClass(如classMockTurtle),派生自待Mock的抽象类(如classTurtle);

对于原抽象类中各待Mock的virtual方法,计算出其参数个数n;

在MockClass类中,使用MOCK_METHODn()(对于const方法则需用MOCK_CONST_METHODn())宏来声明相应的Mock方法,其中第一个参数为待Mock方法的方法名,第二个参数为待Mock方法的类型。如下:

清单4.使用MOCK_METHODn声明Mock方法

#include<gmock/gmock.h>//BringsinGoogleMock.

classMockTurtle:publicTurtle{
MOCK_METHOD0(PenUp,void());
MOCK_METHOD0(PenDown,void());
MOCK_METHOD1(Forward,void(intdistance));
MOCK_METHOD1(Turn,void(intdegrees));
MOCK_METHOD2(GoTo,void(intx,inty));
MOCK_CONST_METHOD0(GetX,int());
MOCK_CONST_METHOD0(GetY,int());
};

在完成上述工作后,就可以开始编写相应的单元测试用例了。在编写单元测试时,可通过ON_CALL宏来指定Mock方法被调用时的行为,或EXPECT_CALL宏来指定Mock方法被调用的次数、被调用时需执行的操作等,并对执行结果进行检查。如下:

清单5.使用ON_CALL及EXPECT_CALL宏

usingtesting::Return;//#1,必要的声明

TEST(BarTest,DoesThis){
MockFoofoo;//#2,创建Mock对象

ON_CALL(foo,GetSize())//#3,设定Mock对象默认的行为(可选)
.WillByDefault(Return(1));
//...otherdefaultactions...

EXPECT_CALL(foo,Describe(5))//#4,设定期望对象被访问的方式及其响应
.Times(3)
.WillRepeatedly(Return("Category5"));
//...otherexpectations...

EXPECT_EQ("good",MyProductionFunction(&foo));
//#5,操作Mock对象并使用googletest提供的断言验证处理结果
}
//#6,当Mock对象被析构时,googlemock会对结果进行验证以判断其行为是否与所有设定的预期一致

其中,WillByDefault用于指定Mock方法被调用时的默认行为;Return用于指定方法被调用时的返回值;Times用于指定方法被调用的次数;WillRepeatedly用于指定方法被调用时重复的行为。

对于未通过EXPECT_CALL声明而被调用的方法,或不满足EXPECT_CALL设定条件的Mock方法调用,googlemock会输出警告信息。对于前一种情况下的警告信息,如果开发者并不关心这些信息,可以使用Adapter类模板NiceMock避免收到这一类警告信息。如下:

清单6.使用NiceMock模板

testing::NiceMock<MockFoo>nice_foo;

在笔者开发的应用中,被测试单元会通过初始化时传入的上层应用的接口指针,产生大量的处理成功或者失败的消息给上层应用,而开发者在编写单元测试时并不关心这些消息的内容,通过使用NiceMock可以避免为不关心的方法编写Mock代码(注意:这些方法仍需在Mock类中声明,否则Mock类会被当作abstractclass而无法实例化)。

与googletest一样,在编写完单元测试后,也需要编写一个如下的入口函数来执行所有的测试:

清单7.初始化googlemock并运行所有测试

#include<gtest/gtest.h>
#include<gmock/gmock.h>

intmain(intargc,char**argv){
testing::InitGoogleMock(&argc,argv);

//RunsalltestsusingGoogleTest.
returnRUN_ALL_TESTS();
}

下面的代码演示了如何使用googlemock来创建MockObjects并设定其行为,从而达到对核心类AccountService的transfer(转账)方法进行单元测试的目的。由于AccountManager类的具体实现涉及数据库等复杂的外部环境,不便直接使用,因此,在编写单元测试时,我们用MockAccountManager替换了具体的AccountManager实现。

清单8.待测试的程序逻辑

//Account.h
//basicapplicationdataclass
#pragmaonce

#include<string>

classAccount
{
private:
std::stringaccountId;

longbalance;

public:
Account();

Account(conststd::string&accountId,longinitialBalance);

voiddebit(longamount);

voidcredit(longamount);

longgetBalance()const;

std::stringgetAccountId()const;
};

//Account.cpp
#include"Account.h"

Account::Account()
{
}

Account::Account(conststd::string&accountId,longinitialBalance)
{
this->accountId=accountId;
this->balance=initialBalance;
}

voidAccount::debit(longamount)
{
this->balance-=amount;
}

voidAccount::credit(longamount)
{
this->balance+=amount;
}

longAccount::getBalance()const
{
returnthis->balance;
}

std::stringAccount::getAccountId()const
{
returnaccountId;
}

//AccountManager.h
//theinterfaceofexternalserviceswhichshouldbemocked
#pragmaonce

#include<string>

#include"Account.h"

classAccountManager
{
public:
virtualAccountfindAccountForUser(conststd::string&userId)=0;

virtualvoidupdateAccount(constAccount&account)=0;
};

//AccountService.h
//theclasstobetested
#pragmaonce

#include<string>

#include"Account.h"
#include"AccountManager.h"

classAccountService
{
private:
AccountManager*pAccountManager;

public:
AccountService();

voidsetAccountManager(AccountManager*pManager);
voidtransfer(conststd::string&senderId,
conststd::string&beneficiaryId,longamount);
};

//AccountService.cpp
#include"AccountService.h"

AccountService::AccountService()
{
this->pAccountManager=NULL;
}

voidAccountService::setAccountManager(AccountManager*pManager)
{
this->pAccountManager=pManager;
}

voidAccountService::transfer(conststd::string&senderId,
conststd::string&beneficiaryId,longamount)
{
Accountsender=this->pAccountManager->findAccountForUser(senderId);

Accountbeneficiary=this->pAccountManager->findAccountForUser(beneficiaryId);

sender.debit(amount);

beneficiary.credit(amount);

this->pAccountManager->updateAccount(sender);

this->pAccountManager->updateAccount(beneficiary);
}

清单9.相应的单元测试

//AccountServiceTest.cpp
//codetotestAccountService
#include<map>
#include<string>

#include<gtest/gtest.h>
#include<gmock/gmock.h>

#include"../Account.h"
#include"../AccountService.h"
#include"../AccountManager.h"

//MockAccountManager,mockAccountManagerwithgooglemock
classMockAccountManager:publicAccountManager
{
public:
MOCK_METHOD1(findAccountForUser,Account(conststd::string&));

MOCK_METHOD1(updateAccount,void(constAccount&));
};

//AfacilityclassactsasanexternalDB
classAccountHelper
{
private:
std::map<std::string,Account>mAccount;
//aninternalmaptostoreallAccountsfortest

public:
AccountHelper(std::map<std::string,Account>&mAccount);

voidupdateAccount(constAccount&account);

AccountfindAccountForUser(conststd::string&userId);
};

AccountHelper::AccountHelper(std::map<std::string,Account>&mAccount)
{
this->mAccount=mAccount;
}

voidAccountHelper::updateAccount(constAccount&account)
{
this->mAccount[account.getAccountId()]=account;
}

AccountAccountHelper::findAccountForUser(conststd::string&userId)
{
if(this->mAccount.find(userId)!=this->mAccount.end())
returnthis->mAccount[userId];
else
returnAccount();
}

//TestcasetotestAccountService
TEST(AccountServiceTest,transferTest)
{
std::map<std::string,Account>mAccount;
mAccount["A"]=Account("A",3000);
mAccount["B"]=Account("B",2000);
AccountHelperhelper(mAccount);

MockAccountManager*pManager=newMockAccountManager();

//specifythebehaviorofMockAccountManager
//alwaysinvokeAccountHelper::findAccountForUser
//whenAccountManager::findAccountForUserisinvoked
EXPECT_CALL(*pManager,findAccountForUser(testing::_)).WillRepeatedly(
testing::Invoke(&helper,&AccountHelper::findAccountForUser));

//alwaysinvokeAccountHelper::updateAccount
//whenAccountManager::updateAccountisinvoked
EXPECT_CALL(*pManager,updateAccount(testing::_)).WillRepeatedly(
testing::Invoke(&helper,&AccountHelper::updateAccount));

AccountServiceas;
//injecttheMockAccountManagerobjectintoAccountService
as.setAccountManager(pManager);

//operateAccountService
as.transfer("A","B",1005);

//checkthebalanceofAccount("A")andAccount("B")to
//verifythatAccountServicehasdonetherightjob
EXPECT_EQ(1995,helper.findAccountForUser("A").getBalance());
EXPECT_EQ(3005,helper.findAccountForUser("B").getBalance());

deletepManager;
}

//Main.cpp
#include<gtest/gtest.h>
#include<gmock/gmock.h>

intmain(intargc,char**argv){
testing::InitGoogleMock(&argc,argv);

//RunsalltestsusingGoogleTest.
returnRUN_ALL_TESTS();
}

注2:上述范例工程详见附件。要编译该工程,请读者自行添加环境变量GTEST_DIR、GMOCK_DIR,分别指向googletest、googlemock解压后所在目录;对于Windows开发者,还需要将%GMOCK_DIR%/msvc/gmock_config.vsprops通过View->PropertyManager添加到工程中,并将gmock.lib拷贝到工程目录下。

通过上面的实例可以看出,googlemock为开发者设定Mock类行为,跟踪程序运行过程及结果,提供了丰富的支持。但与此同时,应用程序也应该尽量降低应用代码间的耦合度,使得单元测试可以很容易对被测试单元进行隔离(如上例中,AccountService必须提供了相应的方法以支持AccountManager的替换)。关于如何通过应用设计模式来降低应用代码间的耦合度,从而编写出易于单元测试的代码,请参考本人的另一篇文章《应用设计模式编写易于单元测试的代码》(
developerWorks,2008年7月)。

注3:此外,开发者也可以直接通过继承被测试类,修改与外围环境相关的方法的实现,达到对其核心方法进行单元测试的目的。但由于这种方法直接改变了被测试类的行为,同时,对被测试类自身的结构有一些要求,因此,适用范围比较小,笔者也并不推荐采用这种原始的Mock方式来进行单元测试。

总结

Googletest与googlemock的组合,很大程度上简化了开发者进行C++应用程序单元测试的编码工作,使得单元测试对于C++开发者也可以变得十分轻松;同时,googletest及googlemock目前仍在不断改进中,相信随着其不断发展,这一C++单元测试的全新组合将变得越来越成熟、越来越强大,也越来越易用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: