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/googletestcd build && cmake .. && make -j 2sudo make install之后只要在/usr/include路径下找到 gtest.h,gmock.h 就说明我们安装成功了。之后只需要在 CMake 之中链接对应的静态库,就可以利用 Gtest 进行单元测试了。
- 用 CMake 生成 Makefile之后直接 make 编译
- 最后进行安装
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 {