预处理——参考《C和指针》第14章

简介: 预处理——参考《C和指针》第14章

一、关于测试

1.1 为啥要测试?

参考链接

1、提高软件的质量

软件测试的首要目的就是提高软件的质量,也就是让用户对产品有更好的体验,保证软件的高质量。

2、保证软件的安全

软件测试的第二大目的就是保证软件的安全,有一些软件是经过数据加密的,比如各大银行系统的APP。涉及到资金的支出和存入,对软件的安全性要求是特别高的。所以要通过反复测试来提高产品的安全性,保证产品在上线之后不会出现bug,尤其对于金融方面的APP来说,任何漏洞都是致命的。

3、降低软件开发成本

软件测试的另外一个目的就是降低软件的开发成本,在开发过程中发现bug及时调整,这样的损失是很小的,一旦产品上线或是即将完成开发而发现bug,那么可能会造成产品大改动,这样就意味着以往的精力全部白费。因此测试的存在就是为了降低开发成本。比如迪士尼的一款狮子王的软件,借着狮子王的名声,预期本应是好评如潮,也能通过这款软件获益不匪。但因为在很多系统上都无法使用,所以造成了大量的用户投诉和下线、卸载等。对成本造成了非常大的损失。那如果当时这款软件能够在不同的系统上进行测试,在上线前将所有的问题全部解决掉,肯定会大大降低成本。

4、降低企业风险

除了降低开发成本,还可以降低企业风险,试想,如果软件存在的问题过多,毫无疑问会影响企业的信誉,最终直接导致企业的合作企业变少,直接损害公司的收益。但如果有测试人员在中间严格把关,就完全不会出现这样的问题。

5、提升用户体验感

开发人员在开发过程中都是以顺向思维来写程序代码的,所以很少有开发人员能够站在用户角度去思考,但测试人员不一样,测试要以逆向思维来思考程序会在哪一步有问题,站在用户的角度进行测试,这样上线的产品将很符合用户的需求,用户使用时也比较顺手,增加用户体验感。

随着产品的不断升级以及用户和公司对软件质量的重视,对品牌和品质意识的提升,软件测试行业也越来越受到大家的重视和青睐,在行业发展趋势里,软件测试像初升的太阳,正在徐徐上升。

1.2 测试的分类

参考链接
请添加图片描述

1.3 [单元测试-参考web[song]的博客]

[参考链接]

1.3.1、为什么单元测试?

客观来说,单元测试和使用版本控制系统(GIT,SVN)是一样重要的。首先我们要明确写代码的两个终极目标来 :

①、“实现需求”

②、“提高代码质量和可维护性”。

结合这两点谈谈为啥要单元测试,单元测试其实就是为了实现代码的第②个目标的一种方法。
(注:代码的可维护性是指增加一个新功能,或改变现有功能的成本,成本越低,可维护性即越高。)

下面以大学四年为例,来谈谈单元测试的重要性和什么时候需要单元测试:

—————————————————大一:Hello World—————————————————
任何一个伟大的程序员都是从最简单的代码开始写起的,假设你的第一个程序是Hello World,任何一个语言实现这个程序都只需要不到5行代码。

这个程序需要单元测试吗?,我们看看这个程序是否实现了软件的两个目标:
1.需求很简单,输出Hello World,这个程序完全满足需求。
2.只有5行代码的“软件”无论是代码质量,还是可维护性,都相当高,你想要把Hello改成Hi真的很轻松。
既然我们已经实现了代码的目标,要不要使用单元测试是无所谓的,同样这么简单的代码也没人会使用GIT或SVN。
代码量:5行

—————————————————大二:简单计算器—————————————————
接下来你写了一个相对更复杂的程序,一个简单计算器。
这个程序实现了数字的加减乘除,整个程序共写了大概50行代码。

这个程序需要单元测试吗?
1.需求是对数字进行加减乘除,这个程序满足了需求。
2.你的代码风格很好(你已经了解到代码风格很重要),你使用了缩进,良好的变量命名,逻辑也清晰,代码的质量和可维护性仍然相当高,如果你想增加一个“求x的平方”功能,你轻而易举就可以做到。
这个时候让你去写单元测试,你仍然会觉得那纯粹是浪费时间。
代码量:50行

—————————————————大三:图书管理系统————————————————
你想要做一个真正的实用系统,给学校开发一个图书管理系统。
你相信这个系统的代码量比起计算器会很多(可能会有1000行)。
你从书上看到有这样一些方法可以简化你的开发工作:
1.工具库(类似你家里的工具箱),使用工具库带来的好处是非常明显的,假如你要实现“返回一个数字数组中的最大值”,你只需要使用某个工具库的Max()函数,只需要1行代码,而不是10行代码自己实现。
2.MVC框架,虽然比起工具库更复杂,需要花更多时间学习,但MVC框架带来的好处也非常明显,轻而易举调用数据库(Model),实现简单的UI界面(View),实现了类似“书名为空的书不允许添加到数据库”的一些逻辑(Controller)。
你最终很好的实现了这个系统,基于MVC模型,你的代码被很好的分割成了很多小的独立的模块:4个Controller,2个Model,4个View。并且在工具库的帮助下,代码量得到了缩减,每个模块大概只有50行代码(等同于一个简单计算器的代码量)。

这个系统需要单元测试吗?
1.你实现了对图书的添加、删除、修改、借阅,你很好的满足了需求(校长表扬了你)。
2.得益于框架与库的使用,你的代码被很好的模块化了,每个模块都像一个“简单计算器”那样简单,增加新功能,或修改现有功能似乎也没有什么大麻烦,虽然会出现一些小bug,但很快就修复了,代码质量和可维护性都比较高。
既然你又实现了代码的目标——“完成需求,高代码质量和可维护性”,那好像也没“单元测试”什么事,毕竟写它要浪费额外的功夫,而且也没感觉到有多少好处。
代码量:500行

————————————————大四:大型库存管理系统———————————————
你被一家IT公司雇佣了,你通过了面试,进入了一个即将开启的项目——为一家大的电商公司做一个库存管理系统。
项目初期一切都很顺利,技术上和你做过的图书管理系统差不多。
首先你了解了客户的需求,然后根据他们的需求,使用你已经掌握的MVC框架和一些库,实现了他们的需求。你写了30个Controller, 50个Model,50个View,每个模块的代码都达到了大概150行,总代码达到了惊人的20000行!
你觉得自己很了不起,能hold住这么多代码,这完全是得益于你的高智商,以及工作努力。客户很满意,老板也很满意,你的自我感觉也很不错。

并且你发现了比单元测试更好的东西,面向对象编程(OOP),或函数式编程(FP),无论是哪一种,你发现你可以把一个模块里的150行堆砌在一起的代码再提取成1个对象的15种方法,或者15个独立的函数(具体怎么提取,你得看相关的书籍),OOP或FP像MVC模型一样,成功的把你的代码分割成了更小的组成部分,每个方法或函数里代码都只有10行左右,你几乎回到了“Hello World”时代。

你需要单元测试吗?(你能保证你的系统没有BUG吗?)

这个复杂系统是由1950个函数和方法组成,如果想要确定系统整体没有BUG,就等同于确定组成这个系统的1950个函数和方法没有BUG。
而单元测试就是做这个事情的,显而易见,如果你写了单元测试,并且每个函数都通过了,你就可以骄傲的说:这个系统没有BUG!(当然这是代码的角度,而非功能和产品的角度)

——————————————————-—结论—————————————————————

虽然,从绝对的角度说,单元测试很重要,但是,从相对的角度来讲,小的代码量,简单固定的需求,个人开发,一锤子买卖等等都会让单元测试显得不那么重要,并且你一直开发的很舒服,这就是为什么有的人感受不到单元测试的重要性(这种情况下的确也许不用写单元测试)。但程序员的智慧是有限的,系统的复杂度却是无限的,随着更大挑战的到来,当系统的复杂度超过了你的逻辑,记忆能力,你必须依靠别的工具来帮助你减少问题。(宇宙中最复杂的系统就是宇宙本身了,假设宇宙是上帝写的系统,上帝可能太聪明了,所以可能没写单元测试,虽然你也是你软件系统的创建者,但你不是上帝)

如果你现在在做一个较大的项目,这个项目的需求很多,所以你一直在开发,你遇到了这样的痛苦状况:1.客户总能在使用中找出BUG,2.每次代码的改动,都会导致一些意想不到的BUG出现。这个时候,单元测试可以挽救你。

1.3.2、[传统方法单元测试-参考魏波的资料]()

有人认为软件编码完成后编写单元测试(传统方法),流程如下:
在这里插入图片描述
缺点: 编写完功能代码再写单元测试会导致单元测试“粒度”比较粗。对同样的功能代码,如果采用TDD方案,结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好,重构时单测的改动不大;如果用传统方法写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。

1.3.3 测试驱动开发(Test-Driven Development,TDD)

后来越来越多的人选择在软件编码之前写单元测试,这种方法成为测试优先或测试驱动开发(Test-Driven Development,TDD),流程如下:

<img src=" title="img">

说明:首先编写一个失败的测试,然后创建产品代码,并确保这个测试通过,接下来是重构代码或创建另一个会失败的测试。具体流程是先写少量功能代码,紧接着写单元测试,重复这两个过程,直到完成功能代码开发。这样做的结果是:基本上功能代码开发完,单元测试也差不多完成了。

(1)编写一个会失败的测试,以证明产品中代码或者功能的缺失。

编写测试的时候,要假设产品代码已经能工作了,这样测试的失败就说明产品代码中有缺陷。这个测试最初会编译失败,只有在添加了需要的代码后,编译才能通过,然后测试可以运行,但是会失败,因为还没有实现所需的功能。

(2)编写符合测试预期的产品代码,使测试通过。
(3)重构代码。

如果测试通过了,你就可以编写下一个单元测试或者进行重构,使代码可读性更强或者去除重复代码等。重构可以在编写多个测试后进行,也可以在每个测试后都进行。重构是一项重要的实践,他确保代码更易读,更好维护,同时还依然能通过之前编写的所有测试。

成功进行TDD需要三种技能:
1、编写优秀的测试,即可维护、可读、可靠。
2、在编码前编写测试。
3、良好的测试设计。

1.3.4、有专业测试部门为啥还要测试?

​ 单元测试是检查代码粒度的bug(一般是以函数和对象的方法为粒度),你可以依赖测试人员,但如果你不想在修改自己一个月前写的代码时自己把自己弄到吐血(或者把别人弄到吐血),最好在当初就写好测试代码,这个工作的责任完全属于程序员。外国已经有很多资深程序员论证了,不论你的单元测试代码质量有多高,覆盖面有多全,单单是你去做这一件事,就可以很大程度的提高你的功能代码的质量,以及大幅减少BUG的存在。

1.3.5、为啥用到测试框架?

​ 在之前的接口重置工作过程中,单元测试这块,都是大部分都是:设定一些值,直接用通讯指令:Get Set Post 一下调用那些接口,然后从控制台观察返回的json是否成功,或者是动态调试观察一些值,看返回的Json是否成功,这样确实可以验证单元模块是否完善,但是这是开发人员自测的时候去人工观察的,并没有一个特定的输出文件,很不规范,所以我们就需要一个测试框架,那为啥要用googletest呢?不自己写一个框架呢?因为自己写框架麻烦,有现成开源的直接拿过来使用就行。虽然轮子造的很爽,但是不是必要的。使用gtest可以免去维护测试框架的麻烦,让我们有更多精力投入到案例设计上。gtest提高了非常完善的功能,并且简单易用,极大的提高了编写测试案例的效率。

为啥要使用:“框架”?

①.不用再考虑公共问题,框架已经帮我们做好了。
②.可以专心用于业务逻辑,保证核心业务逻辑的开发质量。
③.结构统一,便于学习和维护。
④.框架中继承了前人的经验,可以帮助新手写出稳定、性能优良而且结构优美的高质量程序。

1.3.6、好的测试框架具备那些优点?

①测试应该是独立的和可重复的。调试一个由于其他测试而成功或失败的测试是一件痛苦的事情。googletest通过在不同的对象上运行测试来隔离测试。当测试失败时,googletest允许您单独运行它以快速调试。

②测试应该很好地“组织”,并反映出测试代码的结构。googletest将相关测试分组到可以共享数据和子例程的测试套件中。这种通用模式很容易识别,并使测试易于维护。当人们切换项目并开始在新的代码库上工作时,这种一致性尤其有用。

③测试应该是“可移植的”和“可重用的”。谷歌有许多与平台无关的代码;它的测试也应该是平台中立的。googletest可以在不同的操作系统上工作,使用不同的编译器,所以googletest测试可以在多种配置下工作。

④当测试失败时,他们应该提供尽可能多的关于问题的“信息”。谷歌测试不会在第一次测试失败时停止。相反,它只停止当前的测试并继续下一个测试。还可以设置报告非致命失败的测试,在此之后当前测试将继续进行。因此,您可以在一个运行-编辑-编译周期中检测和修复多个错误。总结一下,测试失败时尽可能多地输出错误信息。通过断言,一次测试发现或修复多个问题。

⑤测试框架应该将测试编写者从日常琐事中解放出来,让他们专注于测试“内容”。Googletest自动跟踪所有定义的测试,并且不要求用户为了运行它们而枚举它们。也就是说,开发人员只需要关注测试本身,自动跟踪所有定义测试而不要枚举它们。

⑥测试应该是“快速的”。使用googletest,您可以在测试之间重用共享资源,并且只需要为设置/拆除支付一次费用,而无需使测试彼此依赖。一句话,测试要高效。

二、 GoogleTest的使用

2.1 GoogleTest简介

Google 的开源 C++ 单元测试框架 Google Test,简称 gtest 是一个非常的不错单元测试框架。支持跨平台以及包括 Windows CE 和 Symbian 在内的一些手机操作系统。

2.2 如何搭建GoogleTest

下载好开源项目,编译得到一个lib静态库文件gtestd.lib或是gtest.lib,根据文档的配置步骤,设置相应的工程属性。

2.3 GoogleTest的层次关系

单元测试: 一个项目对于一个单元测试

测试套件: 通常把一组相关的测试称为一个测试套件(test suite)。在实际操作过程中主要是:一个cas,就是一个套件。

测试夹具 Fixture是在软件测试过程中,为测试用例创建其所依赖的前置条件的操作或脚本。

测试案例: 测试用例是一组条件,测试人员根据这些条件确定软件应用程序是否按照客户的要求工作。测试用例设计包括前提条件,用例名称,输入条件和预期结果。

断言: 程序中的一阶逻辑(如:一个结果为真或假的逻辑判断式)

在这里插入图片描述

2.4 GoogleTest的一些“断言”宏

2.4.1 宏的分类

断言的宏可以理解为分为两类,一类是ASSERT系列,一类是EXPECT系列。一个直观的解释就是:

  1. EXPECT_* 失败时,案例继续往下执行。
  2. ASSERT_* 失败时,直接在当前函数中返回,当前函数中ASSERT_*失败了后面的语句将不会执行。

    注意:用户可以直接通过“<<”在这些断言宏后面跟上自己希望在断言命中时的输出信息

    for (int i = 0; i < x.size(); ++i)
    {
        EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index ” <<i;
    }

2.4.2 布尔值的检查

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false

2.4.3 整形数据的检查

Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(expected, actual); EXPECT_EQ(expected, actual); expected == actual
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(val1, val2); EXPECT_GT(val1, val2); val1 〉 val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2

2.4.4 字符串检查

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(expected_str,actual_str); EXPECT_STREQ(expected_str,actual_str); the two C strings have the same content
ASSERT_STRNE(str1, str2); EXPECT_STRNE(str1, str2); the two C strings have different content
ASSERT_STRCASEEQ(expected_str, actual_str); EXPECT_STRCASEEQ(expected_str, actual_str); the two C strings have the same content, ignoring case
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); the two C strings have different content, ignoring case

2.4.5 显示返回成功或失败

直接返回成功

Fatal assertion Nonfatal assertion
SUCCEED(); ADD_SUCCEED();

返回失败:

Fatal assertion Nonfatal assertion
FAIL(); ADD_FAILURE();
TEST(ExplicitTest, Demo)
{
    ADD_FAILURE() << "Sorry"; // None Fatal Asserton,继续往下执行。    //FAIL(); // Fatal Assertion,不往下执行该案例。
    SUCCEED();
}

2.4.6 异常检查

Fatal assertion Nonfatal assertion Verifies
ASSERT_THROW(statement,exception_type); EXPECT_THROW(statement,exception_type); statement throws an exception of the given type
ASSERT_ANY_THROW(statement); EXPECT_ANY_THROW(statement); statement throws an exception of any type
ASSERT_NO_THROW(statement); EXPECT_NO_THROW(statement); statement doesn't throw any exception
int Foo(int a, int b)
{   
    if (a == 0 || b == 0)
    {        throw ”don’t do that";
    }    int c = a % b;    if (c == 0)        return b;    return Foo(b, c);
}

TEST(FooTest, HandleZeroInput)
{
    EXPECT_ANY_THROW(Foo(10, 0));
    EXPECT_THROW(Foo(0, 5), char*);
}

2.4.7 Predicate Assertions

在使用EXPECT_TRUEASSERT_TRUE时,有时希望能够输出更加详细的信息,比如检查一个函数的返回值TRUE还是FALSE时,希望能够输出传入的参数是什么,以便失败后好跟踪。因此提供了如下的断言:

Fatal assertion Nonfatal assertion Verifies
ASSERT_PRED1(pred1, val1); EXPECT_PRED1(pred1, val1); pred1(val1) returns true
ASSERT_PRED2(pred2, val1, val2); EXPECT_PRED2(pred2, val1, val2); pred2(val1, val2) returns true
.. .. ..

Google人说了,他们只提供<=5个参数的,如果需要测试更多的参数,直接告诉他们。下面看看这个东西怎么用。

bool MutuallyPrime(int m, int n)
 {  
    return Foo(m , n) > 1;
 }
 
 TEST(PredicateAssertionTest, Demo)
 {  
        int m = 5, n = 6;
          EXPECT_PRED2(MutuallyPrime, m, n);
 }

当失败时,返回错误信息:

error: MutuallyPrime(m, n) evaluates to false, where
 m evaluates to 5
 n evaluates to 6

如果对这样的输出不满意的话,还可以自定义输出格式,通过如下:

Fatal assertion Nonfatal assertion Verifies
ASSERT_PRED_FORMAT1(pred_format1, val1); EXPECT_PRED_FORMAT1(pred_format1, val1); pred_format1(val1) is successful
ASSERT_PRED_FORMAT2(pred_format2, val1, val2); EXPECT_PRED_FORMAT2(pred_format2, val1, val2); pred_format2(val1, val2) is successful
... ...

用法示例:

testing::AssertionResult AssertFoo(const char* m_expr, const char* n_expr, const char* k_expr, int m, int n, int k) {  
   if (Foo(m, n) == k)   
        return testing::AssertionSuccess();
   testing::Message msg;
           msg << m_expr << ” 和 ” << n_expr << ” 的最大公约数应该是:" <<Foo(m, n) << " 而不是:” <<k_expr; 
           return testing::AssertionFailure(msg);
 }
 TEST(AssertFooTest, HandleFail)
 {
   EXPECT_PRED_FORMAT3(AssertFoo, 3, 6, 2);
 }

失败时,输出信息:

error: 3 和 6 的最大公约数应该是:3 而不是:2

2.4.8 浮点型检查

Fatal assertion Nonfatal assertion Verifies
ASSERT_FLOAT_EQ(expected, actual); EXPECT_FLOAT_EQ(expected, actual); the two float values are almost equal
ASSERT_DOUBLE_EQ(expected, actual); EXPECT_DOUBLE_EQ(expected, actual); the two double values are almost equal

对相近的两个数比较:

Fatal assertion Nonfatal assertion Verifies
ASSERT_NEAR(val1, val2, abs_error); EXPECT_NEAR(val1, val2, abs_error); the difference between val1 and val2 doesn't exceed the given absolute error

同时,还可以使用:

EXPECT_PRED_FORMAT2(testing::FloatLE, val1, val2);
EXPECT_PRED_FORMAT2(testing::DoubleLE, val1, val2);

2.4.9 Windows HRESULT assertions

Fatal assertion Nonfatal assertion Verifies
ASSERT_HRESULT_SUCCEEDED(expression); EXPECT_HRESULT_SUCCEEDED(expression); expression is a success HRESULT
ASSERT_HRESULT_FAILED(expression); EXPECT_HRESULT_FAILED(expression); expression is a failure HRESULT
CComPtr shell;
ASSERT_HRESULT_SUCCEEDED(shell。CoCreateInstance(L”Shell。Application"));
CComVariant empty;
ASSERT_HRESULT_SUCCEEDED(shell—〉ShellExecute(CComBSTR(url), empty, empty, empty, empty));

2.4.10 类型检查

template 〈typename T> class FooType 
{
        public:    void Bar() 
     { 
         testing::StaticAssertTypeEq<int, T〉();
     }
};

TEST(TypeAssertionTest, Demo)
{
    FooType〈bool> fooType;
    fooType.Bar();
}

2.5 “测试用例”宏

2.5.1TESTTEST_F

参考链接

TEST(TestSuit,TestCase)针对一些简单的测试(函数测试、类测试)

TEST_F(TestFixture,TestCase):TEST_F宏用于在多个测试中使用同样的数据配置,所以它又叫:测试夹具(Test Fixtures)

TestFixture 可以供测试案例之间传递数据

① 继承testing::test

TEST_F(TestFixture,TestCase):

同时,通过继承Test类,使用TEST_F宏,我们可以在案例之间共享一些通用方法,共享资源。使得我们的案例更加的简洁,清晰。

三、GoogleTest的事件机制

3.1 前言

gtest提供了多种事件机制,非常方便我们在案例之前或之后做一些操作.总结一下gtest的事件一共有3种:

① 全局的,所有案例执行前后,也就是我们这个单元测试的前后。

TestSuite级别的,在某一批案例中第一个案例前,最后一个案例执行后。

TestCase级别的,每个TestCase前后.

3.2 全局事件

要实现全局事件,必须写一个类,继承testing::Environment类,实现里面的SetUpTearDown方法。

①.SetUp()方法在所有案例执行前执行

②.TearDown()方法在所有案例执行后执行

class FooEnvironment : public testing::Environment
{
    public:    virtual void SetUp()
    {
        std::cout<< ”Foo FooEnvironment SetUP” << std::endl;
    }   
    virtual void TearDown()
    {
        std::cout<<"Foo FooEnvironment TearDown” << std::endl;
    }
};

当然,这样还不够,我们还需要告诉gtest添加这个全局事件,我们需要在main函数中通过testing::AddGlobalTestEnvironment方法将事件挂进来,也就是说,我们可以写很多个这样的类,然后将他们的事件都挂上去。

int _tmain(int argc, _TCHAR* argv[])
{
    testing::AddGlobalTestEnvironment(new FooEnvironment);
    testing::InitGoogleTest(&argc, argv);    
    return RUN_ALL_TESTS();
}

3.3 TestSuite事件

我们需要写一个类,继承testing::Test,然后实现两个静态方法

  1. SetUpTestCase() 方法在第一个TestCase之前执行
  2. TearDownTestCase() 方法在最后一个TestCase之后执行

    class FooTest : public testing::Test {
      protected:  
        static void SetUpTestCase() {
                shared_resource_ = new  ;
          }  
        static void TearDownTestCase() {
                  delete shared_resource_;
                shared_resource_ = NULL;
      }  // Some expensive resource shared by all tests。  static T* shared_resource_;
    };

在编写测试案例时,我们需要使用TEST_F这个宏,第一个参数必须是我们上面类的名字,代表一个TestSuite.

TEST_F(FooTest, Test1)
 {  // you can refer to shared_resource here }
TEST_F(FooTest, Test2)
 {  // you can refer to shared_resource here )

3.4 TestCase事件

TestCase事件是挂在每个案例执行前后的,实现方式和上面的几乎一样,不过需要实现的是SetUp方法和TearDown方法:

SetUp()方法在每个TestCase之前执行

TearDown()方法在每个TestCase之后执行

class FooCalcTest:public testing::Test
{
  protected:    
    virtual void SetUp()
    {
        m_foo.Init();
    }    
    virtual void TearDown()
     {
        m_foo.Finalize();
     }
    FooCalc m_foo;
};

TEST_F(FooCalcTest, HandleNoneZeroInput)
{
    EXPECT_EQ(4, m_foo.Calc(12, 16));
}

TEST_F(FooCalcTest, HandleNoneZeroInput_Error)
{
    EXPECT_EQ(5, m_foo.Calc(12, 16));
}

3.5 GoogleTest的初体验

被测函数:

int Foo(int a, int b)
{    if (a == 0 || b == 0)
    {        throw ”don't do that”;
    }    
         int c = a % b;    
         if (c == 0)        
    return b;    
return Foo(b, c);
}

测试用例(定义:为实施测试,向被测试系统所提供的输入数据、操作或各种环境设置以及期望结果的一个特定的集合。)

#include <gtest/gtest.h>
TEST(FooTest, HandleNoneZeroInput)
{
    EXPECT_EQ(2, Foo(4, 10));
    EXPECT_EQ(6, Foo(30, 18));
}

这两个参数的定义是:[TestSuiteName,TestCaseName],称之为:【测试套件,测试案例】

运行测试案例:

int main(int argc, _TCHAR* argv[])
{
    testing::InitGoogleTest(&argc, argv);    
    return RUN_ALL_TESTS();
}

testing::InitGoogleTest(&argc, argv);

gtest的测试案例允许接收一系列的命令行参数,因此,我们将命令行参数传递给gtest,进行一些初始化操作。gtest的命令行参数非常丰富,在后面我们也会详细了解到。

“RUN_ALL_TESTS()” :运行所有测试案例

OK,一切就绪了,我们直接运行案例试试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uv6bEzRW-1663140730261)(image\结果)]

四、GoolgleTest中的参数化

4.1 旧的方案

​ 在设计测试案例时,经常需要考虑给被测函数传入不同的值的情况。我们之前的做法通常是写一个通用方法,然后编写在测试案例调用它。即使使用了通用方法,这样的工作也是有很多重复性的,程序员都懒,都希望能够少写代码,多复用代码.Google的程序员也一样,他们考虑到了这个问题,并且提供了一个灵活的参数化测试的方案。举个例子,下面检测很多质数,真的要一行一行的输入吗?

TEST(IsPrimeTest, HandleTrueReturn)//检测是否是质数的测试用例
{
    EXPECT_TRUE(IsPrime(3));
    EXPECT_TRUE(IsPrime(5));
    EXPECT_TRUE(IsPrime(11));
    EXPECT_TRUE(IsPrime(23));
    EXPECT_TRUE(IsPrime(17));
}

4.2 新的方案

1.告诉gtest你的参数类型是什么

你必须添加一个类,继承testing::TestWithParam〈T〉,其中T就是你需要参数化的参数类型,比如上面的例子,我需要参数化一个int型的参数

class IsPrimeParamTest : public::testing::TestWithParam<int〉
 {
 
 };

2.告诉gtest你拿到参数的值后,具体做些什么样的测试

这里,我们要使用一个新的宏(嗯,挺兴奋的):TEST_P,关于这个"P"的含义,Google给出的答案非常幽默,就是说你可以理解为"parameterized" 或者 "pattern”。在TEST_P宏里,使用GetParam()获取当前的参数的具体值。

TEST_P(IsPrimeParamTest, HandleTrueReturn)
 {  
       int n = GetParam();
           EXPECT_TRUE(IsPrime(n));
 }

3.告诉gtest你想要测试的参数范围是什么

使用INSTANTIATE_TEST_CASE_P这宏来告诉gtest你要测试的参数范围:

INSTANTIATE_TEST_CASE_P(TrueReturn, IsPrimeParamTest, testing::Values(3, 5, 11, 23, 17));

第一个参数是:测试案例的前缀,可以任意取。

第二个参数是:测试案例的名称,需要和之前定义的参数化的类的名称相同,如:IsPrimeParamTest

第三个参数是:可以理解为参数生成器,上面的例子使用test::Values表示使用括号内的参数。

Google提供了一系列的参数生成的函数:

Range(begin, end[, step]) 范围在begin~end之间,步长为step,不包括end
Values(v1, v2, ..., vN) v1,v2到vN的值
ValuesIn(container) andValuesIn(begin, end) 从一个C类型的数组或是STL容器,或是迭代器中取值
Bool() 取false 和 true 两个值
Combine(g1, g2, ..., gN) 这个比较强悍,它将g1,g2,。。.gN进行排列组合,g1,g2,。。.gN本身是一个参数生成器,每次分别从g1,g2,。.gN中各取出一个值,组合成一个元组(Tuple)作为一个参数。 说明:这个功能只在提供了<tr1/tuple〉头的系统中有效.gtest会自动去判断是否支持tr/tuple,如果你的系统确实支持,而gtest判断错误的话,你可以重新定义宏GTEST_HAS_TR1_TUPLE=1.

4.3 结果

因为使用了参数化的方式执行案例,我非常想知道运行案例时,每个案例名称是如何命名的.我执行了上面的代码,输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XAf3jRtP-1663140730264)(image\参数结果)]

从上面的框框中的案例名称大概能够看出案例的命名规则,对于需要了解每个案例的名称的我来说,这非常重要。 命名规则大概为:

prefix/test_case_name,test,name/index 

4.4 类型参数化

template <typename T>class FooTest : public testing::Test {
   
};
TYPED_TEST_CASE_P(FooTest);
TYPED_TEST_P(FooTest, DoesBlah) {  // Inside a test, refer to TypeParam to get the type parameter。  TypeParam n = 0;  
}
TYPED_TEST_P(FooTest, HasPropertyA) {   }

接着,我们需要我们上面的案例,使用REGISTER_TYPED_TEST_CASE_P宏,第一个参数是testcase的名称,后面的参数是test的名称

REGISTER_TYPED_TEST_CASE_P(FooTest, DoesBlah, HasPropertyA);

接着指定需要的类型列表:

typedef testing::Types<char, int, unsigned int〉 MyTypes;
 INSTANTIATE_TYPED_TEST_CASE_P(My, FooTest, MyTypes);

这种方案相比之前的方案提供更加好的灵活度,当然,框架越灵活,复杂度也会随之增加。

五、GoogleTest运行参数

5.1 设置参数途径

前面提到,对于运行参数,gtest提供了三种设置的途径:

1.系统环境变量

2.命令行参数

3代码中指定FLAG

因为提供了三种途径,就会有优先级的问题, 有一个原则是,最后设置的那个会生效。不过总结一下,通常情况下,比较理想的优先级为:

命令行参数 > 代码中指定FLAG > 系统环境变量

​ 为什么我们编写的测试案例能够处理这些命令行参数呢?是因为我们在main函数中,将命令行参数交给了gtest,由gtest来搞定命令行参数的问题.

int _tmain(int argc, _TCHAR* argv[])
 {
   testing::InitGoogleTest(&argc, argv); 
    return RUN_ALL_TESTS();
 }

​ 这样,我们就拥有了接收和响应gtest命令行参数的能力.如果需要在代码中指定FLAG,可以使用testing::GTEST_FLAG这个宏来设置.比如相对于命令行参数-—gtest_output,可以使用testing::GTEST_FLAG(output) = ”xml:”;来设置。注意到了,不需要加——gtest前缀了。同时,推荐将这句放置InitGoogleTest之前,这样就可以使得对于同样的参数,命令行参数优先级高于代码中指定。

int _tmain(int argc, _TCHAR* argv[])
 {
   testing::GTEST_FLAG(output) = ”xml:”;
   testing::InitGoogleTest(&argc, argv); 
   return RUN_ALL_TESTS();
 }

​ 最后再来说下第一种设置方式-系统环境变量。如果需要gtest的设置系统环境变量,必须注意的是:

1.系统环境变量全大写,比如对于-—gtest_output,响应的系统环境变量为:GTEST_OUTPUT

2.有一个命令行参数例外,那就是——gtest_list_tests,它是不接受系统环境变量的。(只是用来罗列测试案例名称)

5.2 参数列表

5.2.1 测试案例集合

命令行参数 说明
—-gtest_list_tests 使用这个参数时,将不会执行里面的测试案例,而是输出一个案例的列表.

| ——gtest_filter | 对执行的测试案例进行过滤,支持通配符
? 单个字符
* 任意字符
— 排除,如,—a 表示除了a

: 取或,如,a:b 表示a或b

比如下面的例子:
./foo_test 没有指定过滤条件,运行所有案例
./foo_test -—gtest_filter=* 使用通配符*,表示运行所有案例
./foo_test -—gtest_filter=FooTest.* 运行所有“测试案例名称(testcase_name)”为FooTest的案例
./foo_test -—gtest_filter=Null*:Constructor* 运行所有“测试案例名称(testcase_name)”或“测试名称(test_name)”包含Null或Constructor的案例.
./foo_test ——gtest_filter=-*DeathTest。* 运行所有非死亡测试案例。
./foo_test ——gtest_filter=FooTest.*—FooTest。Bar 运行所有“测试案例名称(testcase_name)”为FooTest的案例,但是除了FooTest。Bar这个案例 |
| ——gtest_also_run_disabled_tests | 行案例时,同时也执行被置为无效的测试案例.关于设置测试案例无效的方法为:
在测试案例名称或测试名称中添加DISABLED前缀,比如:

// Tests that Foo does Abc。
TEST(FooTest, DISABLED_DoesAbc) { }

class DISABLED_BarTest : public testing::Test { };

// Tests that Bar does Xyz。
TEST_F(DISABLED_BarTest, DoesXyz) { } |
| ——gtest_repeat=[COUNT] | 设置案例重复运行次数,非常棒的功能!比如:
--gtest_repeat=1000 重复执行1000次,即使中途出现错误。
—-gtest_repeat=—1 无限次数执行。。。。
—-gtest_repeat=1000 ——gtest_break_on_failure 重复执行1000次,并且在第一个错误发生时立即停止。这个功能对调试非常有用。
—-gtest_repeat=1000 —-gtest_filter=FooBar 重复执行1000次测试案例名称为FooBar的案例。 |

5.2.2 测试案例输出

命令行参数 说明
-—gtest_color=(yes\ no|auto) 输出命令行时是否使用一些五颜六色的颜色。默认是auto。
—-gtest_print_time 输出命令行时是否打印每个测试案例的执行时间。默认是不打印的。

| —-gtest_output=xml[:DIRECTORY_PATH\|:FILE_PATH] | 将测试结果输出到一个xml中。
1.——gtest_output=xml: 不指定输出路径时,默认为案例当前路径.

2.——gtest_output=xml:d:\ 指定输出到某个目录

3.—-gtest_output=xml:d:\foo。xml 指定输出到d:\foo。xml

如果不是指定了特定的文件路径,gtest每次输出的报告不会覆盖,而会以数字后缀的方式创建。xml的输出内容后面介绍吧 |

5.2.3 对案例的异常处理

命令行参数 说明
—-gtest_break_on_failure 调试模式下,当案例失败时停止,方便调试
-—gtest_throw_on_failure 当案例失败时以C++异常的方式抛出
——gtest_catch_exceptions 是否捕捉异常.gtest默认是不捕捉异常的,因此假如你的测试案例抛了一个异常,很可能会弹出一个对话框,这非常的不友好,同时也阻碍了测试案例的运行。如果想不弹这个框,可以通过设置这个参数来实现。如将—-gtest_catch_exceptions设置为一个非零的数. 注意:这个参数只在Windows下有效。

六、xml结果可视化

6.1参考Github

开源项目

方法一:
github链接: https://github.com/NeilZhy/gtest-report-prettify
clone该项目代码, 按readme操作, 展示部分截图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ukj4agUW-1663140730264)(image\方案1)]

方法二:
github链接:https://github.com/NeilZhy/gtest2html

在计算机科学中,可扩展样式表转换语言(英语:Extensible Stylesheet Language Transformations,缩写XSLT)是一种样式转换标记语言,可以将XML数据档转换为另外的XML或其它格式,如HTML网页,纯文字。

clone该项目代码, 按readme执行操作, 此处若选择gtest2html.xslt, 则展示效果截图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S28ZqX9M-1663140730265)(image\方案2)]

在readme的指令中, 可以将gtest2html.xslt替换成gtest2html2.xsl, 则展示效果截图如下:

提示: 方法二中使用的实际是一种方式, 只不过使用了不同的配置, 生成的页面效果不一样, 学习了解xslt的语法, 可以按照自己的风格生成多种多样的展示结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3LLzh0M-1663140730265)(image\方案3)]

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"> 

<xsl:output method="html" indent="yes"/> 

<xsl:template match="/"> 

<table cellpadding="2" cellspacing="5" border="1px">
<tr>
    <th bgcolor="#808080"><font color="#FFFFFF">Testcase Num</font></th>
    <th bgcolor="#808080"><font color="#FFFFFF">Failure Num</font></th>
</tr>
<tr>
    <td style="font-family: Verdana; font-size: 15px; font-weight: bold;"><xsl:value-of select="testsuites/@tests"/> </td>
    <td style="font-family: Verdana; font-size: 15px; font-weight: bold;"><xsl:value-of select="testsuites/@failures"/> </td>
</tr>
</table>

<table cellpadding="2" cellspacing="5"> 
<tr><td style="font-family: Verdana; font-size: 10px;">

<table align="left" cellpadding="2" cellspacing="0" style="font-family: Verdana; font-size: 10px;"> 
<tr>
<th bgcolor="#808080"><font color="#FFFFFF"><b>TestSuites</b></font></th> 
<th bgcolor="#808080">
<table width="1000px" align="left" cellpadding="1" cellspacing="0" style="font-family: Verdana; font-size: 10px;">
<tr style="font-family: Verdana; font-size: 10px;">
<td  width="15%"><font color="#FFFFFF"><b>Testcase</b></font></td>
<td  width="25%"><font color="#FFFFFF"><b>Result</b></font></td>
<td  width="75%"><font color="#FFFFFF"><b>ErrorInfo</b></font></td>
</tr>
</table>
</th> 
</tr> 
<xsl:for-each select="testsuites/testsuite"> 
<tr>
<td style="border: 1px solid #808080"><xsl:value-of select="@name"/></td> 
<td style="border: 1px solid #808080">
<table width="1000px" align="left" cellpadding="1" cellspacing="0" style="font-family: Verdana; font-size: 10px;">
<xsl:for-each select="testcase">
<tr>
<td style="border: 1px solid #808080" width="15%" rowspan="@tests"><xsl:value-of select="@name"/></td>
<xsl:choose>
    <xsl:when test="failure">
      <td style="border: 1px solid #808080" bgcolor="#ff00ff" width="25%">Failure</td>
      <td style="border: 1px solid #808080" bgcolor="#ff00ff" width="70%"><xsl:value-of select="failure/@message"/></td>
    </xsl:when>
    <xsl:otherwise>
     <td style="border: 1px solid #808080" width="25%">Success</td>
     <td style="border: 1px solid #808080" width="70%"><xsl:value-of select="failure/@message"/></td>
     </xsl:otherwise>
</xsl:choose>
</tr>
</xsl:for-each>
</table>
</td> 
</tr>
</xsl:for-each> 
</table> 
</td> 
</tr> 
</table>

</xsl:template>
</xsl:stylesheet>

方法三:
github链接: https://github.com/NeilZhy/vjunit
clone项目代码, 按照readme说明操作, 展示截图如下:

需要注意的是, 方法三中的展示结果中没有总结果, 只有每一个测试的结果, 如果想要在结果中展示总结过, 可以修改test.xml, 复制一行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xJNhOEIB-1663140730266)(E:\DeskTop\GoogleTest资料\image\方案3-2)]

七、 关于代码覆盖率

7.1 定义

覆盖率(code coverage rate)是反映测试用例对被测软件覆盖程度的重要指标,也是衡量测试工作进展情况的重要指标。在代码逻辑比较复杂的情况下,测试工作往往只能覆盖到显而易见的逻辑分支,而更多的深层次的逻辑分支则不容易被测试人员发现。为了保证测试的覆盖率,有些开发人员会尝试协助测试人员写出所有的测试用例,这不仅会牺牲大量的宝贵的开发时间,同时也拥有一定的难度,最重要原因就是因为测试难以量化。而代码覆盖工具就是用来量化代码测试的覆盖率,让测试人员可以直观的发现那些没有覆盖到的代码分支。

7.2 覆盖率工具:OpenCppCoverage

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koTUCXr4-1663140730266)(image\覆盖率工具)](插件只支持VS2019版本及以下版本安装)

OpenCppCoverage是Windows平台下开源的C++代码覆盖率工具,使用简单,功能齐全而强大。

用起来非常简单,它不需要在编译时插桩,只需要有pdb文件,运行时插桩,通过OpenCppCoverage启动进程即可。

功能也比较全。

7.2.1 主要特点

①不需要重新编译被测程序,只需要使用openCppCoverage运行程序。

②性能开销比较小。

③按模块、代码路径过滤。

④自动生成html覆盖率结果报告。

⑤支持多个覆盖率结果合并。

7.2.2 使用方法

①. 解压此压缩包

②. 找到VSIXInstaller.exe的路径:我的是:C:\Program Files (x86)\MicrosoftVisualStudio\Installer\resources\app\ServiceHub\Services\Microsoft.VisualStudio.Setup.Service

③. 用cmd执行VSIXInstaller.exe OpenCppCoverage-0.9.7.1.vsix

④. 安装此插件过程中,要等待一分钟后手动点击end task按钮才能完成安装

⑤. 程序写好,编译执行之后,点击“工具 --> Run OpenCppCoverage”,程序运行,将命令行窗口关掉,则代码会出现红色或者绿色的阴影,下面会出现Coverage的报告,如下图:

<img src=" title="img">
在这里插入图片描述

绿色部分表示运行此次程序所覆盖的代码,红色部分表示未覆盖的代码。

八、深入思考GTest

8.1 宏的深入理解

前面的文章已经介绍过TEST宏的用法了,通过TEST宏,我们可以非法简单、方便的编写测试案例,比如:

TEST(FooTest, Demo)
 {
   EXPECT_EQ(1, 1);
 }

TEST宏

  1. TEST宏展开后,是一个继承自testing::Test的类。
  2. 我们在TEST宏里面写的测试代码,其实是被放到了类的TestBody方法中。
  3. 通过静态变量test_info_,调用MakeAndRegisterTestInfo对测试案例进行注册。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yh5Lnoz4-1663140730267)(file:///C:/Users/贾老虎/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpg)]

  4. 上面关键的方法就是MakeAndRegisterTestInfo了,我们跳到MakeAndRegisterTestInfo函数中:
// 创建一个 TestInfo 对象并注册到 Google Test;
// 返回创建的TestInfo对象//
// 参数://
//   test_case_name:            测试案例的名称
//   name:                           测试的名称
//   test_case_comment:       测试案例的注释信息
//   comment:                      测试的注释信息
//   fixture_class_id:             test fixture类的ID
//   set_up_tc:                    事件函数SetUpTestCases的函数地址
//   tear_down_tc:               事件函数TearDownTestCases的函数地址
//   factory:    工厂对象,用于创建测试对象(Test)TestInfo* MakeAndRegisterTestInfo(    const char* test_case_name, const char* name,    const char* test_case_comment, const char* comment,
    TypeId fixture_class_id,
    SetUpTestCaseFunc set_up_tc,
    TearDownTestCaseFunc tear_down_tc,
    TestFactoryBase* factory) {
  TestInfo* const test_info =      new TestInfo(test_case_name, name, test_case_comment, comment,
                   fixture_class_id, factory);
  GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);  
      return test_info;
}

我们看到,上面创建了一个TestInfo对象,然后通过AddTestInfo注册了这个对象.TestInfo对象到底是一个什么样的东西呢?

TestInfo对象主要用于包含如下信息:

  1. 测试案例名称(testcase name)

2.测试名称(test name)

  1. 该案例是否需要执行
  2. 执行案例时,用于创建Test对象的函数指针

5.测试结果

我们还看到,TestInfo的构造函数中,非常重要的一个参数就是工厂对象,它主要负责在运行测试案例时创建出Test对象。我们看到我们上面的例子的factory为:

new ::testing::internal::TestFactoryImpl< FooTest_Demo_Test>

我们明白了,Test对象原来就是TEST宏展开后的那个类的对象(FooTest_Demo_Test),再看看TestFactoryImpl的实现:

template <class TestClass>class TestFactoryImpl : public TestFactoryBase {
public:  virtual Test* CreateTest() 
{
 return new TestClass; }
 };

这个对象工厂够简单吧,嗯,Simple is better。当我们需要创建一个测试对象(Test)时,调用factory的CreateTest()方法就可以了.

创建了TestInfo对象后,再通过下面的方法对TestInfo对象进行注册:

GetUnitTestImpl()—>AddTestInfo(set_up_tc, tear_down_tc, test_info);
GetUnitTestImpl()是获取UnitTestImpl对象:
inline UnitTestImpl* GetUnitTestImpl() {  
return UnitTest::GetInstance()->impl();
 }

其中UnitTest是一个单件(Singleton),整个进程空间只有一个实例,通过UnitTest::GetInstance()获取单件的实例.上面的代码看到,UnitTestImpl对象是最终是从UnitTest对象中获取的.那么UnitTestImpl到底是一个什么样的东西呢?可以这样理解:
UnitTestImpl是一个在UnitTest内部使用的,为执行单元测试案例而提供了一系列实现的那么一个类。(自己归纳的,可能不准确)
我们上面的AddTestInfo就是其中的一个实现,负责注册TestInfo实例:

// 添加TestInfo对象到整个单元测试中//
// 参数:////  set_up_tc:   事件函数SetUpTestCases的函数地址
//  tear_down_tc: 事件函数TearDownTestCases的函数地址
//  test_info:    TestInfo对象
void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
        Test::TearDownTestCaseFunc tear_down_tc,
        TestInfo * test_info) {
        // 处理死亡测试的代码,先不关注它
        if (original_working_dir_.IsEmpty()) {
   original_working_dir_.Set(FilePath::GetCurrentDir());
   if (original_working_dir_。IsEmpty()) {
     printf(”%s\n”, "Failed to get the current working directory.");
     abort();
   }
 }// 获取或创建了一个TestCase对象,并将testinfo添加到TestCase对象中.GetTestCase(test_info—>test_case_name(),
       test_info—>test_case_comment(),
       set_up_tc,
       tear_down_tc)—>AddTestInfo(test_info);
 }

我们看到,TestCase对象出来了,并通过AddTestInfo添加了一个TestInfo对象。这时,似乎豁然开朗了:

  1. TEST宏中的两个参数,第一个参数testcase_name,就是TestCase对象的名称,第二个参数test_name就是Test对象的名称.而TestInfo包含了一个测试案例的一系列信息。

2.一个TestCase对象对应一个或多个TestInfo对象。
在这里插入图片描述
我们来看看TestCase的创建过程(UnitTestImpl::GetTestCase):

// 查找并返回一个指定名称的TestCase对象。如果对象不存在,则创建一个并返回//
// 参数:////  test_case_name:  测试案例名称
//  set_up_tc:      事件函数SetUpTestCases的函数地址
//  tear_down_tc:    事件函数TearDownTestCases的函数地址
TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,                  const char* comment,
                   Test::SetUpTestCaseFunc set_up_tc,
                   Test::TearDownTestCaseFunc tear_down_tc) {
                                    // 从test_cases里查找指定名称的TestCase  internal::ListNode<TestCase*>* node = test_cases_。FindIf(
     TestCaseNameIs(test_case_name));
 
   if (node == NULL) {
     // 没找到,我们来创建一个    TestCase* const test_case =
       new TestCase(test_case_name, comment, set_up_tc, tear_down_tc);
 
     // 判断是否为死亡测试案例   
           if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
                         kDeathTestCaseFilter)) {
       // 是的话,将该案例插入到最后一个死亡测试案例后     
               node = test_cases_.InsertAfter(last_death_test_case_, test_case);
       last_death_test_case_ = node;
     } else {
       // 否则,添加到test_cases最后。     
         test_cases_.PushBack(test_case);
       node = test_cases_。Last();
     }
   }
   // 返回TestCase对象  return node—>element();
 }

8.2 回过头看看TEST宏的定义

#define TEST(test_case_name, test_name)\
   GTEST_TEST_(test_case_name, test_name, \
        ::testing::Test, ::testing::internal::GetTestTypeId())

同时也看看TEST_F宏

\#define TEST_F(test_fixture, test_name)\
   GTEST_TEST_(test_fixture, test_name, test_fixture, \
        ::testing::internal::GetTypeId<test_fixture>())

都是使用了GTEST_TEST_宏,在看看这个宏如何定义的:

\#define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\public:\
   GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\private:\  virtual void TestBody();\  static ::testing::TestInfo* const test_info_;\
   GTEST_DISALLOW_COPY_AND_ASSIGN_(\
     GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
 };\
 \
 ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
   ::test_info_ =\
     ::testing::internal::MakeAndRegisterTestInfo(\
       #test_case_name, #test_name, "", ”", \
       (parent_id), \
       parent_class::SetUpTestCase, \
       parent_class::TearDownTestCase, \      new ::testing::internal::TestFactoryImpl<\
         GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody() 

不需要多解释了,和我们上面展开看到的差不多,不过这里比较明确的看到了,我们在TEST宏里写的就是TestBody里的东西.这里再补充说明一下里面的GTEST_DISALLOW_COPY_AND_ASSIGN_宏,我们上面的例子看出,这个宏展开后:

FooTest_Demo_Test(const FooTest_Demo_Test &);void operator=(const FooTest_Demo_Test &);

正如这个宏的名字一样,它是用于防止对对象进行拷贝和赋值操作的。

8.3 再来了解RUN_ALL_TESTS宏

我们的测试案例的运行就是通过这个宏发起的.RUN_ALL_TEST的定义非常简单:

#define RUN_ALL_TESTS()\
   (::testing::UnitTest::GetInstance()->Run()) 

我们又看到了熟悉的::testing::UnitTest::GetInstance(),看来案例的执行时从UnitTest的Run方法开始的,我提取了一些Run中的关键代码,如下:

int UnitTest::Run() {
   __try {
     return impl_->RunAllTests();
   } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
     GetExceptionCode())) {
     printf(”Exception thrown with code 0x%x。\nFAIL\n”, GetExceptionCode());
     fflush(stdout);
     return 1;
   }
   return impl_->RunAllTests();
 }

我们又看到了熟悉的impl(UnitTestImpl),具体案例该怎么执行,还是得靠UnitTestImpl。

int UnitTestImpl::RunAllTests() {  // ..。
   printer—>OnUnitTestStart(parent_);  // 计时  const TimeInMillis start = GetTimeInMillis();
 
   printer—>OnGlobalSetUpStart(parent_);  // 执行全局的SetUp事件  environments_。ForEach(SetUpEnvironment);
   printer—>OnGlobalSetUpEnd(parent_);  // 全局的SetUp事件执行成功的话  if (!Test::HasFatalFailure()) {    // 执行每个测试案例    test_cases_。ForEach(TestCase::RunTestCase);
   }  // 执行全局的TearDown事件  printer->OnGlobalTearDownStart(parent_);
   environments_in_reverse_order_.ForEach(TearDownEnvironment);
   printer—>OnGlobalTearDownEnd(parent_);
 
   elapsed_time_ = GetTimeInMillis() - start;  // 执行完成  printer—>OnUnitTestEnd(parent_);  // Gets the result and clears it。  if (!Passed()) {
    failed = true;
   }
   ClearResult();  // 返回测试结果  return failed ? 1 : 0;
 } 

上面,我们很开心的看到了我们前面讲到的[全局事件]的调用。environments_是一个Environment的链表结构(List),它的内容是我们在main中通过:

testing::AddGlobalTestEnvironment(new FooEnvironment);

添加进去的。test_cases_我们之前也了解过了,是一个TestCase的链表结构(List).gtest实现了一个链表,并且提供了一个Foreach方法,迭代调用某个函数,并将里面的元素作为函数的参数:

template <typename F> // F is the type of the function/functorvoid ForEach(F functor) const {  for ( const ListNode<E> * node = Head();
      node != NULL;
      node = node->next() ) {
    functor(node->element());
   }
 }

因此,我们关注一下:environments_。ForEach(SetUpEnvironment),其实是迭代调用了SetUpEnvironment函数:

static void SetUpEnvironment(Environment* env) { env—>SetUp(); }

最终调用了我们定义的SetUp()函数。

再看看test_cases_.ForEach(TestCase::RunTestCase)的TestCase::RunTestCase实现:

static void RunTestCase(TestCase * test_case) { test_case—>Run(); }

再看TestCase的Run实现:

void TestCase::Run() {  if (!should_run_) return;  internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
   impl—>set_current_test_case(this);
 
   UnitTestEventListenerInterface * const result_printer =
   impl->result_printer();
 
   result_printer—>OnTestCaseStart(this);
   impl->os_stack_trace_getter()->UponLeavingGTest();  // 哈!SetUpTestCases事件在这里调用 
    set_up_tc_();  const internal::TimeInMillis start = internal::GetTimeInMillis();  // 嗯,前面分析的一个TestCase对应多个TestInfo,因此,在这里迭代对TestInfo调用RunTest方法 
       test_info_list_—>ForEach(internal::TestInfoImpl::RunTest);
   elapsed_time_ = internal::GetTimeInMillis() — start;
 
   impl->os_stack_trace_getter()—>UponLeavingGTest();  // TearDownTestCases事件在这里调用  tear_down_tc_();
   result_printer—>OnTestCaseEnd(this);
   impl—>set_current_test_case(NULL);
 }

第二种事件机制又浮出我们眼前,非常兴奋.可以看出,SetUpTestCases和TearDownTestCaess是在一个TestCase之前和之后调用的。接着看test_info_list_—>ForEach(internal::TestInfoImpl::RunTest):

static void RunTest(TestInfo * test_info) {
   test_info—>impl()->Run();
 }

哦?TestInfo也有一个impl?看来我们之前漏掉了点东西,和UnitTest很类似,TestInfo内部也有一个主管各种实现的类,那就是TestInfoImpl,它在TestInfo的构造函数中创建了出来(还记得前面讲的TestInfo的创建过程吗?):

TestInfo::TestInfo(const char* test_case_name,          const char* name,          const char* test_case_comment,          const char* comment,          internal::TypeId fixture_class_id,          internal::TestFactoryBase* factory) {
   impl_ = new internal::TestInfoImpl(this, test_case_name, name,
                    test_case_comment, comment,
                   fixture_class_id, factory);
 }

因此,案例的执行还得看TestInfoImpl的Run()方法,同样,我简化一下,只列出关键部分的代码:

void TestInfoImpl::Run() {  // 。。。

  UnitTestEventListenerInterface* const result_printer =
     impl—>result_printer();
   result_printer—>OnTestStart(parent_);
   // 开始计时
   const TimeInMillis start = GetTimeInMillis();  Test* test = NULL;
 
   __try {
     // 我们的对象工厂,使用CreateTest()生成Test对象    test = factory_—>CreateTest();
   } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
     GetExceptionCode())) {
     AddExceptionThrownFailure(GetExceptionCode(),               "the test fixture’s constructor");
     return;  }

  // 如果Test对象创建成功  if (!Test::HasFatalFailure()) {

 

​    // 调用Test对象的Run()方法,执行测试案例 

​    test—>Run();
   }
 
   // 执行完毕,删除Test对象  impl—>os_stack_trace_getter()->UponLeavingGTest();
   delete test;
   test = NULL;  // 停止计时
   result_.set_elapsed_time(GetTimeInMillis() — start);  result_printer—>OnTestEnd(parent_);
 }

上面看到了我们前面讲到的对象工厂fatory,通过fatory的CreateTest()方法,创建Test对象,然后执行案例又是通过Test对象的Run()方法:

void Test::Run() { 
               if (!HasSameFixtureClass()) return; 
               internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
   impl—>os_stack_trace_getter()->UponLeavingGTest();
   __try {    // Yeah!每个案例的SetUp事件在这里调用    SetUp();
   } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
     GetExceptionCode())) {
     AddExceptionThrownFailure(GetExceptionCode(), ”SetUp()”);
   }  // We will run the test only if SetUp() had no fatal failure.  if (!HasFatalFailure()) {
     impl->os_stack_trace_getter()—>UponLeavingGTest();
     __try {      // 哈哈!!千辛万苦,我们定义在TEST宏里的东西终于被调用了!      TestBody();
     } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
       GetExceptionCode())) {
       AddExceptionThrownFailure(GetExceptionCode(), "the test body”);
     }
   }
   impl->os_stack_trace_getter()->UponLeavingGTest();
   __try {    // 每个案例的TearDown事件在这里调用    TearDown();
   } __except(internal::UnitTestOptions::GTestShouldProcessSEH(
     GetExceptionCode())) {
     AddExceptionThrownFailure(GetExceptionCode(), "TearDown()”);
   }
 } 

上面的代码里非常极其以及特别的兴奋的看到了执行测试案例的前后事件,测试案例执行TestBody()的代码。仿佛整个gtest的流程在眼前一目了然了.

8.5 总结

本文通过分析TEST宏和RUN_ALL_TEST宏,了解到了整个gtest运作过程,可以说整个过程简洁而优美.之前读《代码之美》,感触颇深,现在读过gtest代码,再次让我感触深刻。记得很早前,我对设计的理解是“功能越强大越好,设计越复杂越好,那样才显得牛”,渐渐得,我才发现,简单才是最好。我曾总结过自己写代码的设计原则:功能明确,设计简单.了解了gtest代码后,猛然发现gtest不就是这样吗,同时gtest也给了我很多惊喜,因此,我对gtest的评价是:功能强大,设计简单,使用方便。

总结一下gtest里的几个关键的对象:

  1. UnitTest 单例,总管整个测试,包括测试环境信息,当前执行状态等等。

2.UnitTestImpl UnitTest内部具体功能的实现者。

  1. Test 我们自己编写的,或通过TEST,TEST_F等宏展开后的Test对象,管理着测试案例的前后事件,具体的执行代码TestBody。

4.TestCase 测试案例对象,管理着基于TestCase的前后事件,管理内部多个TestInfo.

  1. TestInfo 管理着测试案例的基本信息,包括Test对象的创建方法。
  2. TestInfoImpl TestInfo内部具体功能的实现者 。

本文还有很多gtest的细节没有分析到,比如运行参数,死亡测试,跨平台处理,断言的宏等等,希望读者自己把源码下载下来慢慢研究.如本文有错误之处,也请大家指出,谢谢!

九、 如何实现一个测试框架?(代码已经打包)

9.1 前言

上一篇我们分析了gtest的一些内部实现,总的来说整体的流程并不复杂。本篇我们就尝试编写一个精简版本的C++单元测试框架:nancytest ,通过编写这个简单的测试框架,将有助于我们理解gtest.

9.2 整体设计

使用最精简的设计,我们就用两个类,够简单吧:

1. TestCase类
包含单个测试案例的信息.

2. UnitTest类

负责所有测试案例的执行,管理。

9.3 TestCase类

TestCase类包含一个测试案例的基本信息,包括:测试案例名称,测试案例执行结果,同时还提供了测试案例执行的方法.我们编写的测试案例都继承自TestCase类。

#pragma once

class TestCase
{
public:
    TestCase(const char* case_name) : testcase_name(case_name){}

    // 执行测试案例的方法
    virtual void Run() = 0;

    int nTestResult; // 测试案例的执行结果 
    const char* testcase_name; // 测试案例名称
};

9.4 UnitTest类

我们的UnitTest类和gtest的一样,是一个单件。我们的UnitTest类的逻辑非常简单:

1.整个进程空间保存一个UnitTest 的单例。

2.通过RegisterTestCase()将测试案例添加到测试案例集合testcases_中.

  1. 执行测试案例时,调用UnitTest::Run(),遍历测试案例集合testcases_,调用案例的Run()方法
class UnitTest
{
public:
    // 获取单例
    static UnitTest* GetInstance(); 

    // 注册测试案例
    TestCase* RegisterTestCase(TestCase* testcase);
    
    // 执行单元测试    (执行所有的测试案例)
    int Run();

    TestCase* CurrentTestCase; // 记录当前执行的测试案例
    int nTestResult; // 总的执行结果
    int nPassed; // 通过案例数
    int nFailed; // 失败案例数
protected:
    std::vector<TestCase*> testcases_; // 案例集合  (用容器去包含所有的测试代码)
};


下面是UnitTest类的实现:

UnitTest* UnitTest::GetInstance()
 {  
 static UnitTest instance;  return &instance;
 }
 
 TestCase* UnitTest::RegisterTestCase(TestCase* testcase)
 {
   testcases_。push_back(testcase);  
   return testcase;
 }
 nt UnitTest::Run()
{
    nTestResult = 1;
    for (std::vector<TestCase*>::iterator it = testcases_.begin();
        it != testcases_.end(); ++it)
    {
        TestCase* testcase = *it;
        CurrentTestCase = testcase;
        std::cout << "======================================" << std::endl;
        std::cout << "Run TestCase:" << testcase->testcase_name << std::endl;
        testcase->Run();
        std::cout << "End TestCase:" << testcase->testcase_name << std::endl;
        if (testcase->nTestResult)
        {
            nPassed++;
        }
        else
        {
            nFailed++;
            nTestResult = 0;
        }
    }

    std::cout << "======================================" << std::endl;
    std::cout << "Total TestCase : " << nPassed + nFailed << std::endl;
    std::cout << "Passed : " << nPassed << std::endl;
    std::cout << "Failed : " << nFailed << std::endl;
    return nTestResult;
}

9.5 NTEST宏

接下来定一个宏NTEST,方便我们写我们的测试案例的类。

#define TESTCASE_NAME(testcase_name) \
    testcase_name##_TEST

#define NANCY_TEST_(testcase_name) \
class TESTCASE_NAME(testcase_name) : public TestCase \
{ \
public: \
    TESTCASE_NAME(testcase_name)(const char* case_name) : TestCase(case_name){}; \
    virtual void Run(); \
private: \
    static TestCase* const testcase_; \
}; \
\
TestCase* const TESTCASE_NAME(testcase_name) \
    ::testcase_ = UnitTest::GetInstance()->RegisterTestCase( \
        new TESTCASE_NAME(testcase_name)(#testcase_name)); \
void TESTCASE_NAME(testcase_name)::Run()

#define NTEST(testcase_name) \
    NANCY_TEST_(testcase_name)

9.6 RUN_ALL_TEST宏

然后是执行所有测试案例的一个宏:

#define RUN_ALL_TESTS() \
   UnitTest::GetInstance()—>Run();

9.7 断言的宏EXPECT_EQ

这里,我只写一个简单的EXPECT_EQ :

#define EXPECT_EQ(m, n) \
    if (m != n) \
    { \
        UnitTest::GetInstance()->CurrentTestCase->nTestResult = 0; \
        std::cout << "Failed" << std::endl; \
        std::cout << "Expect:" << m << std::endl; \
        std::cout << "Actual:" << n << std::endl; \
    }

9.8 案例Demo

够简单吧,再来看看案例怎么写:

#include ”nancytest。h”int Foo(int a, int b)
 {  return a + b;
 }
 
 NTEST(FooTest_PassDemo)
 {
   EXPECT_EQ(3, Foo(1, 2));
   EXPECT_EQ(2, Foo(1, 1));
 }
 
 NTEST(FooTest_FailDemo)
 {
   EXPECT_EQ(4, Foo(1, 2));
   EXPECT_EQ(2, Foo(1, 2));
 }
  int _tmain(int argc, _TCHAR* argv[])
 { 
             return RUN_ALL_TESTS();
 }
 

整个一山寨版gtest,呵。执行一下,看看结果怎么样:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wy9Xh73Q-1663140730267)(image\自写框架_结果)]

相关文章
|
Unix 编译器 测试技术
预处理——参考《C和指针》第14章
预处理——参考《C和指针》第14章
70 0
|
存储 编译器 API
数组——参考《C和指针》
数组——参考《C和指针》
48 0
|
存储 编译器
数组——参考《C和指针》
数组——参考《C和指针》
76 0
|
C++
2014秋C++ 第15周项目3参考解答 在OJ上玩指针
课程主页在http://blog.csdn.net/sxhelijian/article/details/39152703,课程资源在云学堂“贺老师课堂”同步展示,使用的帐号请到课程主页中查看。  【项目3-在OJ上玩指针】(1)指针的基本操作(1)下面的程序,输入10 100和100 10,均可以输出max=100 min=10,请补充完整程序 #include &lt;iostre
1089 0
|
人工智能 C语言 数据建模
计算机科学-第9周 数组、结构体、指针综合练习 题目及参考解答
《计算机科学》课程主页在:http://blog.csdn.net/sxhelijian/article/details/13705597 发现第9周的题目及参考没有公布,补上。 1、阅读程序阅读下面的程序,写出运行结果,上机时运行程序,记录结果,从而能够理解指针的用法(1) #include&lt;stdio.h&gt; int main(){ char a[]="Hello Wo
1252 0
|
人工智能 存储
计算机科学-第7周 指针及应用 题目及参考解答
《计算机科学》课程主页在:http://blog.csdn.net/sxhelijian/article/details/137055971、阅读程序:阅读下面的程序,写出运行结果,上机时运行程序,记录结果,从而能够理解指针的用法(1)#include&lt;stdio.h&gt; int main() { int a, b, temp; int *p1, *p2; p
1126 0
|
5月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
1月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
25 0
|
2月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
|
3月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)