GoogleTest入门——从搭建到主要功能案例介绍
关键字: 单元测试、gtest、测试夹具、gmock、C++
- 案例简述
由于本组在开发的过程中,单元测试这个环节做得欠佳,主要测试方式停留在手动测试和功能性测试上,并未引入自动化测试工具。
鉴于对于高质量的代码是开发的关键,所以为自己开发的实用类与工具类进行完善的单元测试则显得尤为重要。开发完成的类拥有对应的单元测试类作为辅助,每次进行修改的时候,均需要对单元测试类进行重新构建与运行,以确保修改和新增的功能不对已有功能产生影响。
本组主开发语言C++的单元测试框架中,最为常见并且最好用的就是谷歌公司开发的Gtest框架,此框架中拥有完善的单元测试功能并且单元测试用例书写容易快捷。配合Gmock,可以在联合开发的时候,模拟对方未完成的接口,从而达到模块功能分离,按模块独立测试的形式。最大程度对模块进行解耦,保证当前开发模块的稳定性。
- 案例分析和解决过程
-
GoogleTest与GoogleMock环境搭建
目前Github上最新的release版本为googletest-release-1.10.0。
1.解压github上下载的压缩包。
2.进入压缩包目录下的googletest/src目录,运行
$CXX -std=c++11 -c gtest-all.cc -I ../include -I ../
$AR -crs libgtest.a gtest-all.o
3.生成的libgtest.a即为后续需要使用的静态库。以上步骤如下图1所示:
图1. libgtest.a生成方式
图2. libgmock.a生成方式
4.将libgtest.a,保存至待测环境的lib文件夹中,并将googletest/include目录下的gtest文件夹,拷贝至待测环境中的include文件夹中。将googlemock/include/gmock目录下的所有文件拷贝至待测环境中的include/gtest文件夹中。
备注:
生成动态库也是可以的,但是由于单元测试一般只是测试用,也不会放到板子里实际运行,一般来说推荐单元测试用作静态库方便,但并不是意味着单元测试框架只能使用静态库。这里附上动态库的制作方式,但后文以静态库为例进行讲解。
$CXX -std=c++11 –shared -fPIC gtest-all.cc -I ../include -I ../ -o libgtest.so
备注2:
请注意这里的编译链一定要支持C++11,新版本gtest库已经基于C++11新增完善功能并且优化部分逻辑,如果使用的交叉工具链低于gcc4.8版本,请重新制作交叉工具链。内核Linux2.6.39最高可制作的gcc工具链为4.9.2。详细制作方式可参考之前案例。或者可以直接用x86架构的代码测试逻辑,那么上文的$CXX可以直接使用gcc,这里也需要gcc版本要大于等于4.8。
-
使用GoogleTest和GoogleMock测试基线模块
这里常用的测试工具主要有三个系列,TEST,TEST_F,MOCK_METHODn。以下分别讲解着三个系列工具。
A.TEST系列工具介绍
以本组数据结构中的单元表这个数据结构作为例子,做一个最简单的单元测试介绍,其中单元表的数据结构如后文附录所示。
图3 TEST系列工具使用最简单测试用例介绍
使用googletest测试框架的第一步就是#include "gtest/gtest.h". 接下来即可直接书写测试用例。所有需要应用的文件只在这一个gtest.h里作为引入。
第6-13行为TEST系列工具主体。
这里使用了TEST这个宏,它有两个参数,官方的对这两个参数的解释为:[TestCaseName,TestName],可以理解为测试案例名称和测试名称。一个测试案例中可以包含多个测试。这一点在运行测试程序的时候,报告中会详细打印出来。
一般来说,TestCaseName可以填写为待测试的类名称或者结构体名称,后面的TestName一般填成这个测试用例需要测试的内容含义,例如图3中测试用例,是为了测试初始化,所以起名为Init。这里的名字均不作强制要求填写成什么样子。后续的TEST_F中,第一个名字必须为对应的夹具名称。后面小结会展开描述。
对检查点的检查,上面使用到了EXPECT_EQ、EXPECT_LE、EXPECT_GT、EXPECT_STREQ这个四个工具,分别代表是否相等,前者是否小于等于后者,前者是否大于后者,字符串比较是否相等。更多的工具可以查看源码中的MD文件进行学习,或者进行互联网检索。
Google还包装了一系列EXPECT_* 和ASSERT_*的宏,而EXPECT系列和ASSERT系列的区别是:
1. EXPECT_* 失败时,案例继续往下执行。
2. ASSERT_* 失败时,直接在当前函数中返回,当前函数中ASSERT_*后面的语句将不会执行。
一般来说googletest的main函数均为如下的样子,所以官方其实提供了一个libgmain.a文件,它的实现代码如下:
//gtest-main.cc
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
其中,函数InitGoogleTest负责注册需要运行的所有测试用例,宏RUN_ALL_TEST负责执行所有测试,如果全部成功则返回0,否则返回1。
注释:可以自行在测试代码中添加这个libgmain.a文件,或者每次在测试代码中最后复制粘贴上这一个main函数。由于后续需要继续介绍Gmock,本文暂时按照后者执行。
所以最偷懒的写法就是写完测试用例之后,直接进行测试程序编译,剩下的一切工作都交给Googletest测试框架实现。这里给出最简单的Makefile。值得注意的一点是,pthread这个链接选项一定要开启,因为Googletest测试框架是基于多线程实现的。
图4 makefile写法
编译完成后,运行测试程序,结果如下图所示,整个测试过程中的耗时以及每个测试案件与测试用例的分别耗时,每个测试是否成功,以及所有测试是否通过均会以直观的方式展现出来。
图5 运行结果样例
B.TEST_F系列工具介绍
TEST系列工具是最简单的单元测试系列工具,简单到甚至你会觉得为什么我要废这么大力气学一个这样的玩意,只是为了格式化的输出和一点可笑的时间记录么?的确,如果只有TEST系列工具的确会功能很单薄。这里继续介绍另一个常用工具,测试夹具。
先解释一下,什么叫测试夹具。如果测试一系列东西,都需要一样的环境,那么就没用必要每个测试样品都重复准备一遍环境。就好像如果一只铅笔每次削好才能开始测试(写字效果),那么没有必要每次测试前临场削铅笔。而是可以准备好一些已经削好的放着。
Googltest也支持这样的一个夹具功能,每次测试之前,可以统一提前准备好环境,对于多个有相同测试前提的测试用例进行统一环境准备。
下面以本组中一个类的两个不同功能测试进行演示。
图6 TEST_F系列工具简单使用介绍
图7 TEST_F初始化函数
这里介绍TEST_F系列工具的具体使用,既然是测试夹具,那么刚开始必定是需要将待测物品进行夹紧(SetUp),测试完之后,需要将待测物品进行放松(TearDown)。这两个函数的作用是在测试之前,将待测对象进行相关准备。和测试完毕后,释放相关资源。
SetUp内的语句是在进入TEST_F之前会运行好,然后离开TEST_F之后,TearDown会执行。
注意:夹具类的构建需要共有继承于::testing::Test,并且夹具中的待测对象和一些新建的方法均需要使用protect。
夹具类准备好后,就可以进行测试了。
如图6中第39-49行所示,利用PhaseUnitTest这个夹具类,我们首先进行了ExtendGreen这个接口的测试。对于这个接口,我们每调用一次,应该会让相位的Green()接口的返回结果+1,但是增长到40的时候,再进行增加就不会再增加Green()了,并且还会返回没能增加的时间。本例子中用4个EXPECT_EQ来对这些行为进行了限定。如果他们都能够满足正确,那么说明ExtendGreen接口正常。
接下来,还是这个环境,我们想测试CutGreen接口。普通情况看来,我们需要先还原现场,然后进行重新测试。那么还原现场的行为,我们就不必做了,直接继续用一个新的TEST_F方法,还是用PhaseUnitTest这个夹具类,我们开始测试CutGreen接口。
CutGreen的功能和ExtendGreen相反,每次都是减少Green的返回值,同样,在减少到一定的程度,就不继续减少了。有差别是,在一次性减少很大的值的时候,减少量不会按照最大减小的程度生效,而是不进行操作。
以上就是TEST_F系列工具的使用方式,此工具是首先需要建立一个夹具类,共有继承::testing::Test类,然后再protect中对待测对象进行相关环境准备。然后对于每一个测试用例,均使用TEST_F这个夹具类名称,然后测试名称可以随意取,这里建议取名要能够体现这个测试的内容。
注意:测试夹具类可以无限制的叠加共有继承。也就是说,做好了一个夹具A,可以在A的基础上共有继承夹具B,或者C。所以夹具可以很灵活的进行制作。
C.MOCK系列工具介绍
在联合开发的时候,一般对方给你一些接口,然后告知你一些规则,这个接口会返回什么,第一次会返回什么,然后后面不会返回什么之类的一些限制,然后大家就开始噼里啪啦一顿操作猛如虎的写代码了。当你先写完,然而对方还在捉摸如何下手的时候,你往往会苦恼对方拖后腿了,让你没办法调试代码了。
这个时候MOCK的意义就来了,GMOCK提供了一种规定接口行为的模式,让你MOCK的接口仿佛已经开发完毕一样,并且如果这些接口没有按照约定的规则进行,则会进行报错。有了GMOCK,联合开发的小伙伴开发的再慢也不会影响你自己模块的测试和稳定性了。
Gmock的相关源代码和安装步骤与Gtest十分类似,一样是采取编译成静态库,在需要进行辅助测试的时候,编译选项中加上静态库文件一起编译即可,使用十分方便。
这里用一个简单的例子进行介绍。
图8 Gmock使用介绍
这里FooInterface是联合开发的另一个名小伙伴和你规定的接口,里面有个getArbitrayString()这样的接口,这个小伙伴告诉你,这个接口只能返回3次,每次都会返回Hello World。你一定不能调用第四次,否则就要出问题。
那么我们使用一个MockFoo类,对这个接口进行mock,用方法MOCK_METHOD0来定义这个接口,这个定义的方式是参数1是方法名称,参数2是方法的返回值和传入参数。这里需要额外注意,MOCK_METHODn中的n代表需要mock方法的传入参数个数,这里是0,所以为MOCK_METHOD0。
Mock结束后,MockFoo类就可以开始进行按规则定义了,刚刚小伙伴说这个类只能用3次,而且每次都必定返回Hello World。这里用EXPECT_CALL来定义这个接口的体现形式。
所以我们用
EXPECT_CALL(mockFoo, getArbitraryString())
.Times(3)
.WillRepeatedly(Return(value));
来体现刚刚说的这个规则。
而实际上gmock能够模拟的接口形式有更多种,对接口的入参范围匹配也可以进行操作,具体例子暂时不进行详细讲解。
具体常用的形式如下所示:
// EXPECT_CALL(mock_object, Method(argument-matchers))
// .With(multi-argument-matchers)
// .Times(cardinality)
// .InSequence(sequences)
// .After(expectations)
// .WillOnce(action)
// .WillRepeatedly(action)
// .RetiresOnSaturation();
//
// where all clauses are optional, and .InSequence()/.After()/
// .WillOnce() can appear any number of times.
我们可以使用
EXPECT_CALL声明一个调用期待,就是我们期待这个对象的这个方法按什么样的逻辑去执行。
mock_object是我们mock的对象,上例中就是TestUser的一个对象。
Method是mock对象中的mock方法,它的参数可以通过argument-matchers规则去匹配。
With是多个参数的匹配方式指定。
Times表示这个方法可以被执行多少次。如果超过这个次数,则按默认值返回了。
InSequence用于指定函数执行的顺序。它是通过同一序列中声明期待的顺序确定的。
After方法用于指定某个方法只能在另一个方法之后执行。
WillOnce表示执行一次方法时,将执行其参数action的方法。一般我们使用Return方法,用于指定一次调用的输出。
WillRepeatedly表示一直调用一个方法时,将执行其参数action的方法。需要注意下它和WillOnce的区别,WillOnce是一次,WillRepeatedly是一直。
RetiresOnSaturation用于保证期待调用不会被相同的函数的期待所覆盖。
- 经验总结、预防措施及建议
以上总体介绍完了GoogleTest以及GoogleMock的使用方式,对于测试框架的构建方式,对常用的三种系列进行了详细的举例介绍。
引用一句《google软件测试之道》中的话,“质量不等于测试。当你把开发过程和测试放到一起,就像在搅拌机里混合搅拌那样,直到不能区分彼此的时候,你就得到了质量。”
- 从本文可导出的检查项(checklist)
虽然Googletest测试框架极大程度的简化了作为软件开发人员书写测试案例的难度,但是对于一个优美的测试案例而言,依旧需要做到如下几点:
案例的层次结构一定要清晰
案例的检查点一定要明确
案例失败时一定要能精确的定位问题
案例执行结果一定要稳定
案例执行的时间一定不能太长
案例一定不能对测试环境造成破坏
案例一定独立,不能与其他案例有先后关系的依赖
案例的命名一定清晰,容易理解
参考文献、标准、案例
- https://github.com/google/googletest
- Google 软件测试之道 (惠特克著)
附录:文中使用的代码源码
#include "gtest/gtest.h"
#include
#include "channel.h"
#include "config.h"
#include "phase.h"
#include "gtest/gmock.h"
namespace {
class PhaseUnitTest : public testing::Test {
protected: // You should make the members protected s.t. they can be
// accessed from sub-classes.
virtual void SetUp() override {
TscPhase config;
config.id = 1;
config.desc = "物联网街";
config.greenFlash = 3;
config.yellow = 3;
config.allred = 2;
config.redyellow = 4;
config.pedClear = 4;
config.maxGreen = 40;
config.maxGreen2 = 80;
config.minGreen = 10;
config.unitExtend = 3;
config.checkTime = 4;
config.pedCross = 15;
p1.time = 30;
p1.FillConfig(config, false);
p1.FillColorStep();
}
virtual void TearDown() {
}
Phase p1;
};
// 单元测试通道表
TEST(Channel, Init) {
Channel ch;
EXPECT_EQ(0, ch.id); //相等判断
EXPECT_EQ(0, ch.type);
EXPECT_EQ(0, ch.dir);
EXPECT_GT(ch.id, -1); //大于判断
}
// 单元测试单元表
TEST(TscUnit, Init) {
TscUnit unit;
unit.roadname = "阡陌路";
EXPECT_EQ(0, unit.area);
EXPECT_EQ(0, unit.junction);
EXPECT_EQ(6, unit.bootFlash);
EXPECT_STREQ("阡陌路", unit.roadname.c_str()); //判断
}
// 单元测试相位表ExtendGreen
TEST_F(PhaseUnitTest, ExtendGreen) {
EXPECT_EQ(15, p1.maxExtendTime);
// 延长15次
for (uint8_t i = 0; i < 15; i++) {
EXPECT_EQ(0, p1.ExtendGreen(1));
EXPECT_EQ(26 + i, p1.Green());
}
// 延长第16次,应该无法延长
EXPECT_EQ(1, p1.ExtendGreen(1));
EXPECT_EQ(40, p1.Green());
}
// 单元测试相位表CutGreen
TEST_F(PhaseUnitTest, Move) {
// 相位重新填充后必须移动一秒,这是按照之前的设计必须要的
p1.pedestrian.Move(1);
p1.motor.Move(1);
// 观察初始化后绿灯时间是否正常
EXPECT_EQ(25, p1.Green());
p1.CutGreen(-1);
// 观察缩短一秒后,绿灯时间是否正常
EXPECT_EQ(24, p1.Green());
p1.CutGreen(-1);
// 观察缩短一秒后,绿灯时间是否正常
EXPECT_EQ(23, p1.Green());
p1.CutGreen(-100);
// 观察缩短很大的数字,是否真的不响应
EXPECT_EQ(23, p1.Green());
// 观察缩短很大的数字,是否真的不响应
EXPECT_TRUE(p1.Inuse());
}
}
class FooInterface {
public:
virtual std::string getArbitraryString() = 0;
};
class MockFoo: public FooInterface {
public:
MOCK_METHOD0(getArbitraryString, std::string());
};
using ::testing::Return;
int main(int argc, char** argv) {
::testing::InitGoogleMock(&argc, argv);
// gmock test
string value = "Hello World!";
MockFoo mockFoo;
EXPECT_CALL(mockFoo, getArbitraryString())
.Times(3)
.WillRepeatedly(Return(value));
std::cout << "Returned Value: " << mockFoo.getArbitraryString() << endl;
std::cout << "Returned Value: " << mockFoo.getArbitraryString() << endl;
std::cout << "Returned Value: " << mockFoo.getArbitraryString() << endl;
// std::cout << "Returned Value: " << mockFoo.getArbitraryString() << endl;
// ::testing::InitGoogleTest(&argc, argv); // 在gmock有的时候,这句话可以注释
return RUN_ALL_TESTS();
}