轻松编写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()宏声明测试函数
分别针对同一程序单元GlobalConfiguration声明了两个不同的测试(Test)函数,以分别对配置数据进行检查(configurationDataTest),以及测试没有配置文件的特殊情况(noConfigureFileTest)。
实现单元测试
针对同一程序单元设计出不同的测试场景后(即划分出不同的Test后),开发者就可以编写单元测试分别实现这些测试场景了。
在googletest中实现单元测试,可通过ASSERT_*和EXPECT_*断言来对程序运行结果进行检查。ASSERT_*版本的断言失败时会产生致命失败,并结束当前函数;EXPECT_*版本的断言失败时产生非致命失败,但不会中止当前函数。因此,ASSERT_*常常被用于后续测试逻辑强制依赖的处理结果的断言,如创建对象后检查指针是否为空,若为空,则后续对象方法调用会失败;而EXPECT_*则用于即使失败也不会影响后续测试逻辑的处理结果的断言,如某个方法返回结果的多个属性的检查。
googletest中定义了如下的断言:
表1:googletest定义的断言(Assert)
下面的实例演示了上面部分断言的使用:
清单2.一个较完整的googletest单元测试实例
运行单元测试
在实现完单元测试的测试逻辑后,可以通过RUN_ALL_TESTS()来运行它们,如果所有测试成功,该函数返回0,否则会返回1。RUN_ALL_TESTS()会运行你链接到的所有测试――它们可以来自不同的测试案例,甚至是来自不同的文件。
因此,运行googletest编写的单元测试的一种比较简单可行的方法是:
为每一个被测试的class分别创建一个测试文件,并在该文件中编写针对这一class的单元测试;
编写一个Main.cpp文件,并在其中包含以下代码,以运行所有单元测试:
清单3.初始化googletest并运行所有测试
最后,将所有测试代码及Main.cpp编译并链接到目标程序中。
此外,在运行可执行目标程序时,可以使用--gtest_filter来指定要执行的测试用例,如:
这一特性在包含大量测试用例的项目中会十分有用。
应用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所在的目录,如:
其中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方法
在完成上述工作后,就可以开始编写相应的单元测试用例了。在编写单元测试时,可通过ON_CALL宏来指定Mock方法被调用时的行为,或EXPECT_CALL宏来指定Mock方法被调用的次数、被调用时需执行的操作等,并对执行结果进行检查。如下:
清单5.使用ON_CALL及EXPECT_CALL宏
其中,WillByDefault用于指定Mock方法被调用时的默认行为;Return用于指定方法被调用时的返回值;Times用于指定方法被调用的次数;WillRepeatedly用于指定方法被调用时重复的行为。
对于未通过EXPECT_CALL声明而被调用的方法,或不满足EXPECT_CALL设定条件的Mock方法调用,googlemock会输出警告信息。对于前一种情况下的警告信息,如果开发者并不关心这些信息,可以使用Adapter类模板NiceMock避免收到这一类警告信息。如下:
清单6.使用NiceMock模板
在笔者开发的应用中,被测试单元会通过初始化时传入的上层应用的接口指针,产生大量的处理成功或者失败的消息给上层应用,而开发者在编写单元测试时并不关心这些消息的内容,通过使用NiceMock可以避免为不关心的方法编写Mock代码(注意:这些方法仍需在Mock类中声明,否则Mock类会被当作abstractclass而无法实例化)。
与googletest一样,在编写完单元测试后,也需要编写一个如下的入口函数来执行所有的测试:
清单7.初始化googlemock并运行所有测试
下面的代码演示了如何使用googlemock来创建MockObjects并设定其行为,从而达到对核心类AccountService的transfer(转账)方法进行单元测试的目的。由于AccountManager类的具体实现涉及数据库等复杂的外部环境,不便直接使用,因此,在编写单元测试时,我们用MockAccountManager替换了具体的AccountManager实现。
清单8.待测试的程序逻辑
清单9.相应的单元测试
注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++单元测试的全新组合将变得越来越成熟、越来越强大,也越来越易用。
单元测试概述测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量(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) |
实现单元测试
针对同一程序单元设计出不同的测试场景后(即划分出不同的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(); } |
此外,在运行可执行目标程序时,可以使用--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库的开发者,在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. |
清单5.使用ON_CALL及EXPECT_CALL宏
usingtesting::Return;//#1,必要的声明 |
对于未通过EXPECT_CALL声明而被调用的方法,或不满足EXPECT_CALL设定条件的Mock方法调用,googlemock会输出警告信息。对于前一种情况下的警告信息,如果开发者并不关心这些信息,可以使用Adapter类模板NiceMock避免收到这一类警告信息。如下:
清单6.使用NiceMock模板
testing::NiceMock<MockFoo>nice_foo; |
与googletest一样,在编写完单元测试后,也需要编写一个如下的入口函数来执行所有的测试:
清单7.初始化googlemock并运行所有测试
#include<gtest/gtest.h> |
清单8.待测试的程序逻辑
//Account.h |
//AccountServiceTest.cpp |
通过上面的实例可以看出,googlemock为开发者设定Mock类行为,跟踪程序运行过程及结果,提供了丰富的支持。但与此同时,应用程序也应该尽量降低应用代码间的耦合度,使得单元测试可以很容易对被测试单元进行隔离(如上例中,AccountService必须提供了相应的方法以支持AccountManager的替换)。关于如何通过应用设计模式来降低应用代码间的耦合度,从而编写出易于单元测试的代码,请参考本人的另一篇文章《
developerWorks,2008年7月)。
注3:此外,开发者也可以直接通过继承被测试类,修改与外围环境相关的方法的实现,达到对其核心方法进行单元测试的目的。但由于这种方法直接改变了被测试类的行为,同时,对被测试类自身的结构有一些要求,因此,适用范围比较小,笔者也并不推荐采用这种原始的Mock方式来进行单元测试。
总结
Googletest与googlemock的组合,很大程度上简化了开发者进行C++应用程序单元测试的编码工作,使得单元测试对于C++开发者也可以变得十分轻松;同时,googletest及googlemock目前仍在不断改进中,相信随着其不断发展,这一C++单元测试的全新组合将变得越来越成熟、越来越强大,也越来越易用。
相关文章推荐
- 轻松编写 C++ 单元测试
- 轻松编写 C++ 单元测试
- google test 轻松编写C++单元测试
- 轻松编写 C++ 单元测试 介绍全新单元测试框架组合: googletest 与 googlemock
- 轻松编写 C++ 单元测试
- 轻松编写 C++ 单元测试
- 轻松编写C++单元测试
- [原创翻译]在Visual Studio 中为原生C++代码编写单元测试
- Google开源C++单元测试框架gTest 8:编写简单的测试框架
- Google C++单元测试框架GoogleTest---GTest的Sample1和编写单元测试的步骤
- 编写和执行单元测试
- Effective C++:条款05:了解C++默默编写并调用哪些函数
- 在WPF中,使用C++编写的DLL文件
- 编写综合的单元测试
- (转)[开源世界]图像库MxImage——使用C++模板编写
- 用C++编写一个随机产生多个两位数四则运算式子的简单程序
- vs2013编写c++程序运行闪一下就没了的解决办法
- Linux 下C++编写
- C和C++混合编程的Makefile的编写!
- 关于开发JNI时编写C++找不到to_string()方法的解决办法