正式工做以后,公司对于单元测试要求比较严格。(笔者以前比较懒,通常不多写完整的单测~~)。做为一个合格的开发工程师,须要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工做之中,TDD(Test drivern development) 是一种通过实践可行的开发方式。编写单元测试能够帮助咱们在开发阶段就发现错误,而且保证新的修改没有破坏已有的程序逻辑。 在 C++之中,经常使用的测试框架有 Gtest,Boost test,CPPUint 等。正是因为 Gmock 的加持,让 Gtest 在多种测试框架之中脱颖而出。今天笔者在这里要和你们聊聊的就是目前我司主力在使用的Gtest,以及配套的 Gmock,经过二者的配合使用,相信可以搞定绝大多数的测试场景了。git
笔者目前使用的系统是Deepin 15.6,是基于 Debian jessie的一款国内发行版。安装 Gtest 和 GMock 十分简单:github
sudo apt-get install libgtest-dev libgmock-dev
其余发行版如:Ubuntu,Centos 应该也能够经过自带的包管理软件就能够完成安装了。可是若是包管理软件之中没有带上对应的开发包,也能够选择编译安装:数据库
git clone https://github.com/google/googletest
cd build && cmake .. && make -j 2
sudo make install
以后只要在/usr/include路径下找到gtest.h,gmock.h就说明咱们安装成功了。以后只须要在 CMake 之中连接对应的静态库,就能够利用 Gtest 进行单元测试了。网络
Gtest 十分容易上手,经过其中的定义的宏就能够轻松实现要进行单元测试。而且其中每一个单元测试都会计算出对应执行时间,能够经过执行时间来分析代码的执行效率。app
先举个简单的栗子,假如如今咱们须要测试一下函数来判断质数,代码以下:框架
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适用于多种测试场景须要相同数据配置的状况,利用了 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 的使用应该可以知足绝大多数小型项目的测试场景了。可是对于一些涉及数据库交互,网络通讯的大型项目的测试场景,咱们很难仿真一个真实的环境进行单元测试。因此这时就须要引入** 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。它接受两个参数:
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 的时候须要多加注意。
经过Gtest 与 Gmock 的使用,可以覆盖绝大多数进行 C++ 单元测试的场景,同时也减小了咱们编写单元测试的工做。笔者但愿经过本篇文章来抛砖引玉,但愿你们多写单测。在笔者实际的工做经验之中,单测给项目带来的影响是极其正面的,必定要坚持写单测,坚持写单测,坚持写单测~~~!!!