Gtest 与 Gmock,聊聊C++的单元测试

正式工作之后,公司对于单元测试要求比较严格。(笔者之前比较懒,一般很少写完整的单测~~)。作为一个合格的开发工程师,需要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工作之中,TDD(Test drivern development) 是一种经过实践可行的开发方式。编写单元测试可以帮助我们在开发阶段就发现错误,并且保证新的修改没有破坏已有的程序逻辑。在 C++之中,常用的测试框架有Gtest,Boost test,CPPUint等。正是由于 Gmock 的加持,让 Gtest 在多种测试框架之中脱颖而出。今天笔者在这里要和大家聊聊的就是目前我司主力在使用的Gtest,以及配套的Gmock,通过两者的配合使用,相信能够搞定绝大多数的测试场景了。

创一个小群,供大家学习交流聊天

如果有对学C++方面有什么疑惑问题的,或者有什么想说的想聊的大家可以一起交流学习一起进步呀。

也希望大家对学C++能够持之以恒

C++爱好群,

如果你想要学好C++最好加入一个组织,这样大家学习的话就比较方便,还能够共同交流和分享资料,给你推荐一个学习的组织:快乐学习C++组织 可以点击组织二字,可以直达

1、Gtest 的安装

笔者目前使用的系统是Deepin 15.6,是基于Debian jessie的一款国内发行版。安装 Gtest 和 GMock 十分简单:

sudo apt-getinstall libgtest-dev libgmock-dev

其他发行版如:Ubuntu,Centos应该也可以通过自带的包管理软件就可以完成安装了。但是如果包管理软件之中没有带上对应的开发包,也可以选择编译安装:

下载源码

gitclone https://github.com/google/googletest

用 CMake 生成 Makefile之后直接 make 编译

cdbuild && cmake .. && make -j 2

最后进行安装

sudomake install

之后只要在/usr/include路径下找到gtest.h,gmock.h就说明我们安装成功了。之后只需要在 CMake 之中链接对应的静态库,就可以利用 Gtest 进行单元测试了。

2、Gtest 的使用

Gtest 十分容易上手,通过其中的定义的宏就可以轻松实现要进行单元测试。并且其中每个单元测试都会计算出对应执行时间,可以通过执行时间来分析代码的执行效率。

测试函数TEST

先举个简单的栗子,假如现在我们需要测试一下函数来判断质数,代码如下:

boolis_prime(intnum) {

if(num<2)

returnfalse;

for(inti =2; i <= sqrt(num) +1; i++) {

if(num% i ==0)

returnfalse;

}

returntrue;

}

现在我们用 Gtest 对这个函数进行测试,TEST的宏定义代表了会被RUN_ALL_TESTS执行的测试函数。在 Gtest 之中提供了两类断言ASSERT_*系列和EXPECT_*系列。两者的区别就在于,ASSERT失败之后就不会运行后续的测试了,但是EXPECT虽然失败,但是不影响后续测试的进行。看起来EXPECT会更加灵活一些,尤其是需要释放一些资源或执行一些其他逻辑时,更适合用EXPECT

TEST(test_prime, is_true) {

EXPECT_TRUE(is_prime(5));

ASSERT_TRUE(is_prime(5));

EXPECT_TRUE(is_prime(3));

}

TEST(test_prime, is_false) {

ASSERT_FALSE(is_prime(4));

EXPECT_FALSE(is_prime(4));

}

intmain(intargc,char*argv[]){

testing::InitGoogleTest(&argc, argv);

RUN_ALL_TESTS();

testing::InitGoogleTest初始化测试框架,必须在运行测试之前调用RUN_ALL_TESTS会运行所有由TEST 宏定义的测试。测试结果如下图所示:我们定义的is_true和 is_false同属同一个测试 case:test_prime,并且成功通过了测试。

测试结果

上面我们使用了这TRUE 与 FALSE 的判断宏:

判断宏

Gtest 提供了多种的判断宏,包括字符串的判断,数值判断等等,具体的细节可以参照 Gtest 的官方文档https://github.com/google/googletest/blob/master/googletest/docs/primer.md,笔者这里不再赘述。

测试函数TEST_F

很多时候,我们进行一些测试的时候需要进行资源初始化工作,进行资源复用,最后回收资源。这样的场景就适合使用TEST_F的宏来进行测试。TEST_F适用于多种测试场景需要相同数据配置的情况,利用了 C++继承类来实现对父类方法的测试。举个例子,笔者实现了一个跳表,下面是对跳表进行测试的代码:

classTest_Skiplist:publictesting::Test {

public:

virtualvoidSetUp(){

std::cout<<"Set Up Test"<

_sl->load();

}

virtualvoidTearDown(){

std::cout<<"Tear Down Test"<

_sl->dump();

}

~Test_Skiplist(){};

SkipList* _sl =newSkipList();

};

TEST_F(Test_Skiplist, insert) {

std::stringtest_string("happen");

ASSERT_EQ(_sl->insert("1", test_string.c_str(), test_string.size()), Status::SUCCESS);

test_string ="lee";

ASSERT_EQ(_sl->insert("2", test_string.c_str(), test_string.size()), Status::SUCCESS);

uint64_tdata64 =50;

ASSERT_EQ(_sl->insert("50",reinterpret_cast(&data64),8), Status::SUCCESS);

uint32_tdata32 =20;

ASSERT_EQ(_sl->insert("20",reinterpret_cast(&data32),4), Status::SUCCESS);

}

TEST_F(Test_Skiplist, search) {

ASSERT_EQ(_sl->search("1")->value_size,6);

ASSERT_STREQ(std::string(_sl->search("1")->value.get()).c_str(),"happen");

ASSERT_EQ(_sl->search("3"),nullptr);

}

TEST_F(Test_Skiplist, remove) {

ASSERT_EQ(_sl->remove("1"), Status::SUCCESS);

ASSERT_EQ(_sl->remove("1"), Status::FAIL);

ASSERT_EQ(_sl->search("1"),nullptr);

}

由上述代码可以看到,通过TEST_F进行的测试类需要继承testing::Test类。同时要实现对应的SetUpTearDown方法,SetUp方式执行资源的初始化操作,而TearDown则负责资源的释放。需要注意的是,上述代码我们测试了跳表的search,remove,insert方法,而每个测试是独立的进行的。也就是每个测试执行时都会运行对应的SetUp和 TearDown方法。

命令行参数

编译生成二进制的测试执行文件之后,直接运行就可以执行单元测试了。但是 Gtest 提供了一些命令行参数来帮助我们更好的使用,下面介绍一下笔者常用的几个命令行参数:

--gtest_list_tests

列出所有需要执行的测试,但是并不执行。

--gtest_filter

对所执行的测试进行过滤,支持通配符

?      单个字符

*      任意字符

-      排除

./test --gtest_filter=SkTest.*-SkTest.insert表示运行所有名为SkTest的案例,排除了SkTest.insert这个案例。

--gtest_repeat=[count]

设置测试重复运行的次数,其中-1表示无限执行。

3、Gmock 的使用

上述 Gtest 的使用应该能够满足绝大多数小型项目的测试场景了。但是对于一些涉及数据库交互,网络通信的大型项目的测试场景,我们很难仿真一个真实的环境进行单元测试。所以这时就需要引入** Mock Objects **(模拟对象)来模拟程序的一部分来构造测试场景。Mock Object模拟了实际对象的接口,通过一些简单的代码模拟实际对象部分的逻辑,实现起来简单很多。通过 Mock object 的方式可以更好的提升项目的模块化程度,隔离不同的程序逻辑或环境。

至于如何使用 Mock Object 呢?这里要引出本章的主角 Gmock 了,接下来笔者将编写一个简要的 Mock对象并进行单元测试,来展示一下 GMock 的用法。这里我们用 Gmock 模拟一个 kv 存储引擎,并运行一些简单的测试逻辑。下面的代码是我们要模拟的 kv 存储引擎的头文件:

#ifndefLDB_KVDB_MOCK_H

#defineLDB_KVDB_MOCK_H

classKVDB{

public:

std::stringget(conststd::string&key)const;

Statusset(conststd::string&key,conststd::string&value);

Statusremove(conststd::string&key);

};

#endif//LDB_KVDB_MOCK_H

然后我们需要定义个 Mock 类来继承 KVDB,并且定义需要模拟的方法:get, set, remove。这里我们用到了宏定义MOCK_METHOD,后面的数字代表了模拟函数的参数个数,如MOCK_METHOD0MOCK_METHOD1。它接受两个参数:

参数1,方法名称。

参数2,函数的指针的定义

classMockKVDB:publicKVDB {

public:

MockKVDB() {

}

MOCK_METHOD1(remove, Status(conststd::string&));

MOCK_METHOD2(set, Status(conststd::string&,conststd::string&));

MOCK_METHOD1(get,std::string(conststd::string&));

};

通过这个宏定义,我们已经初步模拟出对应的方法了。接下来我们需要告诉 Mock Object 被调用时的正确行为。

TEST_F(TestKVDB, setstr) {

EXPECT_CALL(*kvdb, set(_,_)).WillRepeatedly(Return(Status::SUCCESS));

ASSERT_EQ(kvdb->set("1","happen"), Status::SUCCESS);

ASSERT_EQ(kvdb->set("2","lee"), Status::SUCCESS);

ASSERT_EQ(kvdb->set("happen","1"), Status::SUCCESS);

ASSERT_EQ(kvdb->set("lee","2"), Status::SUCCESS);

}

TEST_F(TestKVDB, getstr) {

EXPECT_CALL(*kvdb, get(_)) \

.WillOnce(Return("happen"))

.WillOnce(Return("lee"))

.WillOnce(Return("1"))

.WillOnce(Return("2"));

ASSERT_STREQ(kvdb->get("1").c_str(),"happen");

ASSERT_STREQ(kvdb->get("2").c_str(),"lee");

ASSERT_STREQ(kvdb->get("happen").c_str(),"1");

ASSERT_STREQ(kvdb->get("lee").c_str(),"2");

}

TEST_F(TestKVDB, remove) {

EXPECT_CALL(*kvdb, remove(_)).WillOnce(Return(Status::SUCCESS)).

WillOnce(Return(Status::FAIL));

EXPECT_CALL(*kvdb, get(_)) \

.WillOnce(Return(""));

ASSERT_EQ(kvdb->remove("1"), Status::SUCCESS);

ASSERT_EQ(kvdb->get("1"),"");

ASSERT_EQ(kvdb->remove("1"), Status::FAIL);

}

由上述代码可以了解,这里通过了EXPECT_CALL来指定 Mock Object 的对应行为,其中WillOnce代表调用一次返回的结果。通过链式调用的方式,我们就可以构造出所有我们想要的模拟结果。当然如果每次调用的结果都一样,这里也可以使用WillRepeatedly直接返回对应的结果。由上述代码可以看到,这里我们用寥寥数十行代码就模拟出了一个 KV 存储引擎,可见 Gmock 的强大。

这里要注意,在通过 Gmock 来编写 Mock Object 时,能够模拟的方法是对于原抽象类之中的virtual方法。这个是因为C++只有通过virtual的方式才能实现子类覆写的多态,这一点在编写代码进行抽象和编写 Mock Object 的时候需要多加注意。

4、小结

通过Gtest 与 Gmock 的使用,能够覆盖绝大多数进行 C++ 单元测试的场景,同时也减少了我们编写单元测试的工作。笔者希望通过本篇文章来抛砖引玉,希望大家多写单测。在笔者实际的工作经验之中,单测给项目带来的影响是极其正面的,一定要坚持写单测,坚持写单测,坚持写单测~~~!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容