C++雾中风景番外篇2:Gtest 与 Gmock,聊聊C++的单元测试
正式工作之后,公司对于单元测试要求比较严格。(笔者之前比较懒,一般很少写完整的单测~~)。作为一个合格的开发工程师,需要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工作之中,TDD(Test drivern development) 是一种经过实践可行的开发方式。编写单元测试可以帮助我们在开发阶段就发现错误,并且保证新的修改没有破坏已有的程序逻辑。 在 C++之中,常用的测试框架有 Gtest,Boost test,CPPUint 等。正是由于 Gmock 的加持,让 Gtest 在多种测试框架之中脱颖而出。今天笔者在这里要和大家聊聊的就是目前我司主力在使用的Gtest,以及配套的 Gmock,通过两者的配合使用,相信能够搞定绝大多数的测试场景了。
1.Gtest 的安装
笔者目前使用的系统是Deepin 15.6,是基于 Debian jessie的一款国内发行版。安装 Gtest 和 GMock 十分简单:
sudo apt-get install libgtest-dev libgmock-dev
其他发行版如:Ubuntu,Centos 应该也可以通过自带的包管理软件就可以完成安装了。但是如果包管理软件之中没有带上对应的开发包,也可以选择编译安装:
- 下载源码
git clone https://github.com/google/googletest
- 用 CMake 生成 Makefile之后直接 make 编译
cd build && cmake .. && make -j 2
- 最后进行安装
sudo make install
之后只要在/usr/include路径下找到gtest.h,gmock.h就说明我们安装成功了。之后只需要在 CMake 之中链接对应的静态库,就可以利用 Gtest 进行单元测试了。
2.Gtest 的使用
Gtest 十分容易上手,通过其中的定义的宏就可以轻松实现要进行单元测试。并且其中每个单元测试都会计算出对应执行时间,可以通过执行时间来分析代码的执行效率。
测试函数TEST
先举个简单的栗子,假如现在我们需要测试一下函数来判断质数,代码如下:
bool is_prime(int num) {
if (num < 2)
return false;
for(int i = 2; i <= sqrt(num) + 1; i++) {
if (num % i == 0)
return false;
}
return true;
}
现在我们用 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));
}
int main(int argc,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 的官方文档,笔者这里不再赘述。
测试函数TEST_F
很多时候,我们进行一些测试的时候需要进行资源初始化工作,进行资源复用,最后回收资源。这样的场景就适合使用 TEST_F的宏来进行测试。TEST_F适用于多种测试场景需要相同数据配置的情况,利用了 C++继承类来实现对父类方法的测试。举个例子,笔者实现了一个跳表,下面是对跳表进行测试的代码:
class Test_Skiplist : public testing::Test {
public:
virtual void SetUp() {
std::cout << "Set Up Test" << std::endl;
_sl->load();
}
virtual void TearDown() {
std::cout << "Tear Down Test" << std::endl;
_sl->dump();
}
~Test_Skiplist(){};
SkipList* _sl = new SkipList();
};
TEST_F(Test_Skiplist, insert) {
std::string test_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_t data64 = 50;
ASSERT_EQ(_sl->insert("50", reinterpret_cast<char *>(&data64), 8), Status::SUCCESS);
uint32_t data32 = 20;
ASSERT_EQ(_sl->insert("20", reinterpret_cast<char *>(&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类。同时要实现对应的 SetUp与TearDown方法,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 存储引擎的头文件:
#ifndef LDB_KVDB_MOCK_H
#define LDB_KVDB_MOCK_H
class KVDB {
public:
std::string get(const std::string &key) const;
Status set(const std::string &key, const std::string &value);
Status remove(const std::string &key);
};
#endif //LDB_KVDB_MOCK_H
然后我们需要定义个 Mock 类来继承 KVDB,并且定义需要模拟的方法:get, set, remove。这里我们用到了宏定义 MOCK_METHOD,后面的数字代表了模拟函数的参数个数,如MOCK_METHOD0,MOCK_METHOD1。它接受两个参数:
- 参数1,方法名称。
- 参数2,函数的指针的定义
class MockKVDB : public KVDB {
public:
MockKVDB() {
}
MOCK_METHOD1(remove, Status(const std::string&));
MOCK_METHOD2(set, Status(const std::string&, const std::string&));
MOCK_METHOD1(get, std::string (const std::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++ 单元测试的场景,同时也减少了我们编写单元测试的工作。笔者希望通过本篇文章来抛砖引玉,希望大家多写单测。在笔者实际的工作经验之中,单测给项目带来的影响是极其正面的,一定要坚持写单测,坚持写单测,坚持写单测~~~!!!
C++雾中风景番外篇2:Gtest 与 Gmock,聊聊C++的单元测试的更多相关文章
- C++雾中风景番外篇:理解C++的复杂声明与声明解析
在学习C系列语言的过程之中,理解C/C++的复杂声明一直是初学者很困扰的问题.笔者初学之时也深受困扰,对很多规则死记硬背.后续在阅读<C专家编程>之后,尝试在编译器的角度来理解C/C++的 ...
- C++雾中风景番外篇3:GDB与Valgrind ,调试代码内存的工具
写 C++的同学想必有太多和内存打交道的血泪经验了,常常被 C++的内存问题搅的焦头烂额.(写 core 的经验了)有很多同学一见到 core 就两眼一抹黑,不知所措了.笔者 入"坑&quo ...
- C++雾中风景番外篇4:GCC升级二三事
最近将手头上负责的项目代码从GCC 4.8.2升级到了GCC 8.2.(终于可以使用C++17了,想想后续的开发也是很美好啊~~)不过这个过程之中也遇到了一些稀奇古怪的问题,在这里做一个简单的记录,希 ...
- 【番外篇】ASP.NET MVC快速入门之免费jQuery控件库(MVC5+EF6)
目录 [第一篇]ASP.NET MVC快速入门之数据库操作(MVC5+EF6) [第二篇]ASP.NET MVC快速入门之数据注解(MVC5+EF6) [第三篇]ASP.NET MVC快速入门之安全策 ...
- iOS冰与火之歌(番外篇) - 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权
iOS冰与火之歌(番外篇) 基于PEGASUS(Trident三叉戟)的OS X 10.11.6本地提权 蒸米@阿里移动安全 0x00 序 这段时间最火的漏洞当属阿联酋的人权活动人士被apt攻击所使用 ...
- 给深度学习入门者的Python快速教程 - 番外篇之Python-OpenCV
这次博客园的排版彻底残了..高清版请移步: https://zhuanlan.zhihu.com/p/24425116 本篇是前面两篇教程: 给深度学习入门者的Python快速教程 - 基础篇 给深度 ...
- 可视化(番外篇)——在Eclipse RCP中玩转OpenGL
最近在看有关Eclipse RCP方面的东西,鉴于Gephi是使用opengl作为绘图引擎,所以,萌生了在Eclipse RCP下添加画布,使用opengl绘图的想法,网上有博文详细介绍这方面的内容, ...
- 可视化(番外篇)——SWT总结
本篇主要介绍如何在SWT下构建一个应用,如何安装SWT Designer并破解已进行SWT的可视化编程,Display以及Shell为何物.有何用,SWT中的常用组件.面板容器以及事件模型等. 1.可 ...
- 【重走Android之路】【番外篇】关于==和equals
[重走Android之路][番外篇]关于==和equals 在实际的编程当中,经常会使用==和equals来判断变量是否相同.但是这两种比较方式也常常让人搞得云里雾里摸不着头脑.下面是我个人做的总 ...
随机推荐
- XMLHttpRequest: 网络错误 0x2f78,…00002f78
常在河边走,怎能不湿脚,在web前端开发的过程中总是遇到很多关于IE的故事. 一个get请求,传了一个json对象,包含一串参数,在IE上就出现了这个问题:XMLHttpRequest: 网络错误 0 ...
- maven配置jdk1.8环境
<!-- 局部jdk配置,pom.xml中 --> <build> <plugins> <plugin> <groupId>org.apac ...
- IOS中的三大事件
iOS 中,所有显示在界面上的对象都是从 UIResponder 直接或间接继承的,只有继承了它才可以处理事件.而在ios中的事件可以分为三大类: 1.触摸事件 2.加速计事件(摇一摇) 3.远程控制 ...
- Dom4j向XML中指定位置添加、删除、修改节点——(五)
需求: 在第一本书作者后面增加描述 <描述>好书</描述> 思路:获取书下面下的所有节点(一个list集合),在list集合指定位置添加一个元素(list.add(index ...
- Docker安装Zookeeper
⒈下载 docker pull zookeeper ⒉运行 docker run --name zk -p 2181:2181 -p 2888:2888 -p 3888:3888 --restart ...
- 【ARTS】01_11_左耳听风-20190121~20190127
ARTS: Algrothm: leetcode算法题目 Review: 阅读并且点评一篇英文技术文章 Tip/Techni: 学习一个技术技巧 Share: 分享一篇有观点和思考的技术文章 Algo ...
- python模块分析之typing(三)
前言:很多人在写完代码一段时间后回过头看代码,很可能忘记了自己写的函数需要传什么参数,返回什么类型的结果,就不得不去阅读代码的具体内容,降低了阅读的速度,加上Python本身就是一门弱类型的语言,这种 ...
- Python3学习笔记09-字典
字典是另一种可变容器模型,且可存储任意类型对象. 字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中 键必须是唯一的,但值则不必 ...
- System.Runtime.InteropServices.COMException (0x800A03EC): 无法访问文件
使用Microsoft.Office.Interop.Excel 操作 今天在服务器部署,操作程序csv文件转xsl文件的时候,遇到一下问题: System.Runtime.InteropServic ...
- Windows 10 的一些快捷键
Win键 + Q: 呼出[Cortana] Win键 + W:呼出[Windows INNK 工作区] Win键 + E: 呼出[资源管理器] Win键 + R: 呼出[运行] Win键 + A: 呼 ...