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++ 单元测试的场景,同时也减少了我们编写单元测试的工作。笔者希望通过本篇文章来抛砖引玉,希望大家多写单测。在笔者实际的工作经验之中,单测给项目带来的影响是极其正面的,一定要坚持写单测,坚持写单测,坚持写单测~~~!!!