C++雾中风景番外篇2:Gtest 与 Gmock,聊聊C++的单元测试

  • 时间:2020-04-24 21:09 作者:LeeHappen 来源: 阅读:591
  • 扫一扫,手机访问
摘要:正式工作之后,公司对于单元测试要求比较严格。(笔者之前比较懒,一般很少写完整的单测~~)。作为一个合格的开发工程师,需要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工作之中,TDD(Test drivern development) 是一种经过实践可行的开发方式。编写单元测试可以帮助

正式工作之后,公司对于单元测试要求比较严格。(笔者之前比较懒,一般很少写完整的单测~~)。作为一个合格的开发工程师,需要为所编写代码编写适量的单元测试是十分必要的,在实际进行的开发工作之中,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  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类。同时要实现对应的 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 存储引擎的头文件:

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

  • 全部评论(0)
最新发布的资讯信息
【系统环境|】2FA验证器 验证码如何登录(2024-04-01 20:18)
【系统环境|】怎么做才能建设好外贸网站?(2023-12-20 10:05)
【系统环境|数据库】 潮玩宇宙游戏道具收集方法(2023-12-12 16:13)
【系统环境|】遥遥领先!青否数字人直播系统5.0发布,支持真人接管实时驱动!(2023-10-12 17:31)
【系统环境|服务器应用】克隆自己的数字人形象需要几步?(2023-09-20 17:13)
【系统环境|】Tiktok登录教程(2023-02-13 14:17)
【系统环境|】ZORRO佐罗软件安装教程及一键新机使用方法详细简介(2023-02-10 21:56)
【系统环境|】阿里云 centos 云盘扩容命令(2023-01-10 16:35)
【系统环境|】补单系统搭建补单源码搭建(2022-05-18 11:35)
【系统环境|服务器应用】高端显卡再度登上热搜,竟然是因为“断崖式”的降价(2022-04-12 19:47)
手机二维码手机访问领取大礼包
返回顶部