Google单元测试工具及其框架

gtest框架使用及总结

Posted by VK on July 25, 2019

Google单元测试工具及其框架

1. 什么是单元测试

单元测试的本质其实也是代码,与我们平常的普通代码的区别在于它是用于验证代码正确性的代码。即:单元测试是由开发人员自行编写的用于检测在特定条件下目标代码正确性的代码。

说到底,开发人员终究是人,不是机器,那就注定无法像机器那样精准,开发人员参与到的项目数不胜数,庞大的代码量所造成的复杂性是不言而喻的,没有人敢保证自己写出来的代码一点问题也没有,或者不经测试就能保证代码正确运行直接上线,也许这个代码你在某个执行路径或环境下能够完美执行,殊不知还有其他路径或者运行环境,有一一去验证过吗?

尽管行业里有不少的开发人员都常常把“Bug是无穷无尽的,解不完的”挂在嘴边调侃,甚至连微软公司的windows系统似乎也在佐证着这一点,windows系统这么多年过去了,仍然还有问题,仍需不停的更新修复。但这并不是我们可以疏忽的理由,正因如此,我们更要严谨保证程序的正确性,必须对我们的代码进行严格测试。

2. 为什么要做单元测试

通常我们在做任何工作会先考虑它的回报,编写代码更是如此。如果单元测试的作用不大,没有人会愿意再写一堆无用的代码,那么单元测试到底能够给我们带来什么优点呢?如下:

  • 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。
  • 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。
  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。
  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。

无非就是良好的接口设计、正确性、可回归、可测试、完善的调用文档、高内聚、低耦合,这些优点已经足以让我们对单元测试重视起来了,但是个人觉得还有更重要的原因。

  • 首先,带来自信。在接手一个新的项目,或者说是参与一个新的项目开发时,往往这种情况是你半途参加进去的,你需要对已有的代码结构进行解读和理解,对于业务的理解,对于代码个中各个模块关系的理解。如果一开始就理解出错,很可能修改后的代码会引起更多的BUG出现,到那时候又需要修复更多的BUG,改了一个地方,很有可能会莫名其妙地影响另外一个地方,这种现象是很常见的。还有一种情况,假设你修改的功能没问题,但是需要去测试验证,在测试的时候就需要考虑这个功能点它原有的测试路径有哪些,又需要一一去验证功能路径,以证明本次修改对于已存在的功能点不造成影响。这其中就存在着很大的时间成本,导致效率不高。那是否存在着这么一种方式,我需要修改我想改动的地方,不需要关心修改完之后它所造成的影响,也不需要关心它的测试回归性,有,此时就是单元测试登场的时候。写单元测试代码,可以让我自己写的代码足够自信,它是经得起考验的。
  • 其次,更快反馈。对于有一定编程经验的开发人员来说,当他拿到一个新需求的时候,首先想到的不是动手 Coding ,而是会先想想代码的结构,有些类,数据结构该是如何,然后才开始敲代码。如果没有单元测试,一般流程基本是这个模块功能全部写完才开始测试,比如利用 MVP 架构的功能。一般都是开始 Model 模块,然后完善 Presenter 模块,最后写 View 模块,等这几个模块都写完了,再把 APP 跑起来,验证自己写的功能模块是否符合需求,没有符合则继续回去修改代码,这中间需要花费很长的时间才能知道当下自己写的代码是否符合要求,是否正确。那有没有一种即时反馈的方式呢,有,写单元测试即可,当你写完一个函数,马上就匹配一个单元测试函数,这样即写即测的方式可以保证你当场写的代码马上进行修改,测试通过一个,就表示完成一个小的功能点,最后,把函数组装起来,就是我们想要大的功能点。
  • 最后,节约时间。对于 Android 开发来说,一遍一遍的运行 APP ,然后执行相应的用户操作,看界面是否正确的显示,通过这种方式来测试功能,其实是非常浪费时间,而且效率不高,而用单元测试,可以几乎不用打开 APP 来执行,当然有些需要一些资源文件的是需要 APP 运行条件,绝大部分的功能在单元测试阶段就能验证完毕,那么速度就相对快很多。此外,单元测试还能帮忙减少 BUG ,从而减少调试 BUG 的时间,一些低级犯的错误在单元测试阶段就能避免掉。

3. 不写单元测试借口

​ 很多开发人员不写单元测试,最重要的一个原因是他们并不知道单元测试能够带来什么好处,甚至根本不了解单元测试这个词,那自然就像平行线般与之毫无交集。还有一个比较重要的原因是一些开发人员的编程思想还处在一个相对初级的阶段,开发软件只管实现功能,什么高内聚、低耦合、重构、设计、可测试等认为太过专业,对于这些名词以及意义还不了解,这自然不会考虑使用了。还有一些非思想层面的理由,如下:

  • 单元测试太花时间了。软件开发工作那么忙,代码都写不完哪有时间写单元测试。这可能是开发人员用的最多的借口,从某些方面来说,这不能算借口,因为很多开发人员确实在工作上投入的时间特别多。但真的是这样的吗,你有没有想过,导致加班的原因也许就是花了太多时间在手动测试、调试程序上:或许你没有考虑到灵活性与设计,使得在需求发生变更时你需要花很多时间在复杂的代码堆中完成特定的功能,而这些修改又可能引入新的 BUG ,又将导致你需要进行耗时的手动测试、调试等等,如此反复,代码将变得越来越乱,越来越难以维护,最终导致无休止的加班。
  • 测试不是我的工作。测试确实不是开发人员的工作,但单元测试确实是开发人员的工作,测试包含很多种,而只有单元测试是开发人员的工作范畴。开发人员为应用编写代码,那么自然需要保证代码的正确性,而单元测试正是这种保证代码正确性的白盒测试,也就是在了解代码内部结构逻辑的情况下进行有目的的测试,既然说到了解代码,那么开发者自然是最权威的人。因此,编写单元测试并且为测试人员提交正确的代码进行其他测试是开发人员的职责所在。
  • 代码都编译通过了,还测什么。一般来说,这是一个不会放在嘴上但可能藏在心里的借口。代码编译通过只能说你写的代码符合语法要求,并不代表能保证正确性。
  • 代码原来就没有单元测试,并且难以测试。这个问题基本是接受和维护别人开发的代码,而原来的代码本身就没有单元测试了,再加入如果代码的耦合性较高,那么就更难以为这些代码写单元测试。此时正是你了解代码时候,首先为能够测试的部分添加单元测试,保证这些可测试的部分不会被污染,然后在对代码有足够的了解之后再对代码进行重构,降低代码的耦合性,并且慢慢补充测试用例,使得代码的耦合性、可测试性慢慢建立起来

4. Google单元测试主流框架gtest

4.1 什么是gtest

gtest是一个跨平台的(Liunx、Mac OS X、Windows 、Cygwin 、Windows CE and Symbian ) C++单元测试框架,由google公司发布。gtest是为在不同平台上为编写C++测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化、”死亡测试”等等。

4.2 安装gtest

可以点击以上链接在官网下载,也可以直接通过shell命令安装,我是通过shell命令直接安装的,执行命令如下:

$sudo apt-get install libgtest-dev
$sudo cd /usr/src/gtest
$sudo cmake .
$sudo make
$sudo cp libgtest*.a /usr/local/lib

4.3 gtest使用规则说明

4.3.1 gtest之TEST宏

TEST(test_case_name, test_name)
TEST_F(test_fixture,test_name) //多个测试场景需要相同数据配置的情况,用TEST_F。TEST_F test fixture,测试夹具,测试套,承担了一个注册的功能。
4.3.1.1 TEST与TEST_F之间的区别

TEST_F比TEST强一些的地方在于TEST_F实际上会生成一个新类,该类有SetUp和TearDown函数用于建立和销毁数据结构。

同一个TestCase文件中不能混合使用TEST与TEST_F。

For each test defined with TEST_F(), Google Test will:

Create a fresh test fixture at runtime Immediately initialize it via SetUp() , Run the test Clean up by calling TearDown()

Delete the test fixture. Note that different tests in the same test case have different test fixture objects, and Google Test always deletes a test fixture before it creates the next one. Google Test does not reuse the same test fixture for multiple tests. Any changes one test makes to the fixture do not affect other tests.

4.3.2 gtest之断言

要测试一个类或函数,我们需要对其行为做出断言。当一个断言失败时,Google Test会在屏幕上输出该代码所在的源文件及其所在的位置行号,以及错误信息。也可以在编写断言时,提供一个自定义的错误信息,这个信息在失败时会被附加在Google Test的错误信息之后。

断言常常成对出现,它们都测试同一个类或者函数,但对当前功能有着不同的效果。ASSERT_版本的断言失败时会产生致命失败,并结束当前函数。EXPECT_版本的断言产生非致命失败,而不会中止当前函数。通常更推荐使用EXPECT_断言,因为它们运行一个测试中可以有不止一个的错误被报告出来。但如果在编写断言如果失败,就没有必要继续往下执行的测试时,你应该使用ASSERT_断言。 因为失败的ASSERT_*断言会立刻从当前的函数返回,可能会跳过其后的一些的清洁代码,这样也许会导致空间泄漏。

gtest中断言的宏可以分为两类:一类是ASSERT宏,另一类就是EXPECT宏了。
1、ASSERT_*系列:如果当前点检测失败则退出当前函数
2、EXPECT_*系列:如果当前点检测失败则继续往下执行

根据以上就很容易明白什么情况选择ASSERT_; 什么情况下应该选择EXPECT_;

4.3.2.1 gtest断言分类
4.3.2.1.1 Boolean断言类型
致命断言 非致命断言 结果
ASSERT_TRUE(condition) EXPECT_TRUE(condition) condition 为true
ASSERT_FALSE(condition) EXPECT_FALSE(condition) condition 为false
4.3.2.1.2 二元值断言类型

比较两个值的大小。

致命断言 非致命断言 判断结果
ASSERT_EQ(val1,val2) EXPECT_EQ(val1, val2) val1 = val2
ASSERT_NE(val1, val2) EXPECT_NE(val1, val2) val1 != val2
ASSERT_LT(val1, val2) EXPECT_LT(val1, val2) val1 < val2
ASSERT_LE(val1, val2) EXPECT_LE(val1, val2) val1 <= val2
ASSERT_GT() EXPECT_GT() val1 > val2
ASSERT_GE(val1, val2) EXPECT_GE(val1, val2) val1 >= val2
4.3.2.1.3 字符串断言类型

比较两个字符串。

致命断言 非致命断言 判断结果
ASSERT_STREQ(val1, val2) EXPECT_STREQ(val1, val2) val1 == val2
ASSERT_STRNE() EXPECT_STRNE() val1 != val2
ASSERT_STRCASEEQ() EXPECT_STRCASEEQ() 忽略大小写val1 == val2
ASSERT_STRCASENE() EXPECT_STRCASENE() 忽略大小写 val1 != val2

4.4 gtest demo简单示例

#include <gtest/gtest.h>

class A
{
private:
    int _a;
public:
    A(int a);
    ~A();
    void add(int a);
    int getA();
};



A::A(int a)
{
    _a = a;
}

A::~A()
{

}

void A::add(int a)
{
    _a += a;
}

int A::getA()
{
    return _a;
}


class A_test:public testing::Test{
protected:
    A* _p_a;
    virtual void SetUp(){
        _p_a = new A(1);
    }

    virtual void TearDown(){
        delete _p_a;
    }

};

TEST_F(A_test, FirstAdd)
{
    EXPECT_EQ(1,_p_a->getA());
    _p_a->add(3);
    EXPECT_EQ(4,_p_a->getA());
}


TEST_F(A_test, SecondAdd)
{
    EXPECT_EQ(1,_p_a->getA());
    _p_a->add(2);
    EXPECT_EQ(4,_p_a->getA());    
}


TEST_F(A_test, ThirdAdd)
{
    EXPECT_EQ(1,_p_a->getA());
    _p_a->add(2);
    EXPECT_EQ(3,_p_a->getA());    
}


/*TEST不能和TEST_F混用,否则运行会报错*/
#if 0
TEST(A_test, ForthAdd)
{
    A* _p_a = new A(1);
    EXPECT_EQ(1,_p_a->getA());
    _p_a->add(2);
    EXPECT_EQ(3,_p_a->getA());    
}
#endif


/*
上面的两个测试都是在SetUp函数执行后的状态下执行,也就是说在执行任意一个TEST_F时 _p_a->_a 的值都是初始值1
*/
int main(int argc,char* argv[])
{
    testing::InitGoogleTest(&argc,argv);
    return RUN_ALL_TESTS();
}

4.5 小结

TEST()并TEST_F()使用googletest隐式注册他们的测试。因此,与许多其他C ++测试框架不同,您不必重新列出所有已定义的测试以便运行它们。在定义测试之后,您可以使用RUN_ALL_TESTS()它来运行它们,0如果所有测试都成功,1则返回它们。请注意,在链接单元中 RUN_ALL_TESTS()运行所有测试 - 它们可以来自不同的测试用例,甚至是不同的源文件。调用时,RUN_ALL_TESTS()宏:保存所有googletest标志的状态为第一次测试创建测试夹具对象,通过初始化它SetUp()在夹具对象上运行测试。通过清理夹具TearDown()删除夹具。恢复所有googletest标志的状态,重复上述步骤进行下一次测试,直到所有测试都运行完毕。如果发生致命故障,将跳过后续步骤。

重要提示:您不能忽略返回值RUN_ALL_TESTS(),否则您将收到编译器错误。此设计的基本原理是自动化测试服务根据其退出代码确定测试是否已通过,而不是根据其stdout / stderr输出; 因此你的main()函数必须返回值RUN_ALL_TESTS()。 此外,你应该RUN_ALL_TESTS()只打一次电话。多次调用它会与某些高级googletest功能(例如线程安全死亡测试)冲突,因此不受支持。

其实Gtest相比Gmock的使用是简单的多了,主要合理的使用以上断言就能为自己的程序写出一个自动化的测试流程;

如果需要设置期望,可以调用EXPECT_CALL,该方法是gmock中的成员。如果需要mock进行模拟类的创建请查看下一章gmock的用法,本人在实际项目中应用更多的是gmock用法,因为gmock更适合模拟涉及到和其他模块交互的过程测试,下一章也会附上项目源码分享,给大家提供参考;