C++单元测试的一点感悟

来源:互联网 发布:linux ping 100个包 编辑:程序博客网 时间:2024/05/17 03:59

      之前一直在寻找一种合适的方法来做C++单元测试,也尝试了不少的方法。写一点体会提供大家参考(不一定是最好的,但是我想还是能给大家一些启发吧)。JAVA和C#都有强大的IDE支持,而且JAVA和C#的反射机制能够使得Mock更加容易一些。但是由于C/C++语言的独特性,单元测试的过程变得不那么的顺手,特别是工作在Linux的程序猿们,可能还在使用最原始的文本编辑器VI+Makefile来编写自己的代码。而且在实际的C++的项目中,很多程序员做的所谓的单元测试,其正确的定义应该为集成测试或者接口测试。因为,他们针对的测试更多的是从最上层的接口调用来展开的(当然,这里不是绝对,我只是以我个人的经历举例说明)。此篇文章将结合本人多年的C++编程经验和测试经验来探讨和寻找一种更加合理的C++单元测试的方法,使得C++单元测试更可行一些。

首先,我们分析一下在C++单元测试实践过程中,可能会面临如下的非常实际的情况:

1.   一个工程会包含很多类,这些类又相互依赖。这些关系可能是错综复杂的,没有办法分离。

2.   一个工程可能会调用或者依赖第三方的服务,而第三方的服务在项目初期可能无法使用或者在测试环境中无法使用。

3.   如何组织测试代码和被测代码,并且将测试代码引入到工程,而且测试代码不能影响开发代码。

4.   采用何种方式编译测试代码,而且测试程序完全独立于产品程序

本篇文章将围绕上述4个问题来集中的分析和展开,共同探讨更加可行的单元测试方案。此篇文章只是基于本人的个人经历,也许您会有更合适或者合理的方法,欢迎共同探讨。

目前,市面上也提供了很多单元测试框架的选择,如:CppUnit,CppUnitLite,GTest。但是,当你去网上Google或者Baidu,希望能够得到一些有用的实例时,你可能会发现这些实例都不是你想要的。因为他们仅仅停留在最基本的几个案例上,几乎和hello world差不多。而你的项目可能是很复杂的,有大量的依赖,无法套用这些简单的实例。本篇文章将结合本人实际的项目经验来逐一探讨以下几种解决方案,来分析各种解决方案的利弊,寻找更加可行的解决方案。(以下给出的案例分析,将从整体架构设计上给出具体的分析,测试框架以GTest为例。

产品架构:


                                                     Sample产品架构示意图

上图是Sample产品的类结构图。如果我们需要对Class C做单元测试,那么Class C依赖Class B,MySQL Utils,ThirdService,并且Class B还继承于Class A。我们将依次给出3种解决方案来分析各种解决方案的优缺点。

 

方案一:产品代码分离测试

将Class C的代码单独测试并且不包含其他任何产品代码。由于Class C依赖Class B,MySQLUtils,第三方服务,那么意味着你的测试代码需要Mock Class B, Mock MySQLUtils, Mock Third Service。以下是测试程序的结构:



从上图的结构不难看出,此方案除了测试代码还需要写大量的Mock代码。而且编写Mock代码可能是非常耗费时间和经历的,从某种程度上来说,此方案加大了测试的复杂性和工作量。对于快速迭代的产品,比如互联网公司的一些特性,需要在一至两周内迭代发布产品。这样的单元测试设计可能不是太实际。但是,此方案最大的优势也在于Mock的强制性。首先,依赖的服务的Mock率必须是100%,因为你不把所有依赖的服务都Mock掉,是无法通过编译的。其次,Mock可以无需借助于任何的框架,只需将Mock类的定义以及函数定义写得和被Mock的真实类一模一样就行了,因为实际的测试代码并未将真实的依赖产品代码一同编译。而且也必须这样做,如果不Mock的一模一样是无法通过编译的。而且一些私有或者函数内部变量也是可以被Mock的,如下代码:

string GetToken(stringusername)

{

      ThirdService svc;

      svc.GetToken(username);

      …….

}

用过 gmock的同学可能会了解,如果ThirdService(第三方服务)目前不可用的情况下,使用gmock是没法将ThirdService Mock掉的。gmock采用的是继承的方式,而ThirdService在函数内部,将无法将被Mock的ThirdService的实例传入。而我们讨论的方案一,为了测试该函数,你需要重新实现一个ThirdService,其所有类命名和函数类名都必须和真实的ThirdService一模一样,实现可能不一样,根据测试要求安排。那么,方案一使得GetToken函数可测(在“单元测试设计”文章中会集中讨论如何编写你代码使得代码可测)

方案一的特点是:

   1.   依赖全量Mock,测试成本高,难度较大

   2.   依赖服务相对较小的项目

   3.   测试覆盖面广,代码可测性强

   4.   适合代码质量要求高,并且开发时间充裕的项目

 

方案二:测试代码和产品代码一体化

方案一在复杂和多依赖的项目中,需要大量的Mock工作,增加了测试的复杂度。如果我们将测试代码和产品的整个工程编译在一起,这样测试代码就可以调用到产品代码几乎所有资源,解决掉依赖的问题。



从方案二可以看出,大量的Mock已经消失。测试代码和产品所有的代码编译在一起,最大的好处就是能够拿到产品几乎所有的资源(当然不包括哪些private资源或者内部变量等),对public的函数做单元测试已经可以满足需求了。对所有的函数做单元测试(包括:private函数),我个人觉得只是一种理想的状态。很多情况由于项目这样那样的原因,只能保证部分函数或者核心代码的单元测试。当然对private的函数也是有方法去做单元测试的,本篇不做讨论。

那方案二是完美的吗?答案:非也。方案二有一个严重的问题,以gtest框架为例,gtest要求main()函数中初始化gtest框架和执行RUN_ALL_TESTS()。从上图可以看出,产品的代码Main.cpp已经包含了产品的main()函数。那么,方案二意味着需要修改产品的代码将gtest初始化和RUN_ALL_TESTS()加入至产品的main()函数中。可能你首先会想到用宏开关控制,如果当前编译的是测试代码,定义一个测试宏,并将gtest的初始化和RUN_ALL_TESTS()编译。如果当前编译的是产品代码,则gtest的初始化和RUN_ALL_TESTS()不进行编译。这样确实能够满足要求,但是这样会破坏产品代码的纯洁性。我们的目标是产品代码不会包含任何的测试代码,保证产品代码的纯洁性。所以,方案二也不是那样的完美,那我们能否做到对产品代码零修改呢?接下来,我们看方案三。

 

方案三:产品main()函数自动剥离

方案二已经减轻了Mock的工作量,但是由于需要修改产品的main()函数,打破了产品代码的纯洁性。在方案三中将利用编译器的优化功能来解决此问题。首先,我们看一下方案三的架构设计:


方案三首先将产品代码编译成一个static库(sample.a),而非执行程序,然后j将其和测试代码链接成一个测试执行程序。你可能会问这样就可以解决方案二的问题吗?细心的读者可能会发现上图中有两个main()函数,在测试代码TestC.cpp中包含了一个main()函数,该main()函数中添加了gtest初始化和RUN_ALL_TESTS()。在产品代码Main.cpp中也有一个产品的main()函数。那一个执行程序可以包含两个main()函数吗?答案:“当然不行”。这里利用了编译器的一点点小技巧,编译器在链接一个静态库时,发现静态库中的main()函数和目标代码中的main()函数冲突时,编译器会自动的将静态库中的main()函数剥离掉。最终的执行程序只会保留目标代码中的main()函数,即TestC.cpp中的main()函数。但是,前提是静态库中的Main.cpp没有函数被别的函数调用。其实编译器做的事情等同于将Main.o删除掉了,但是如果Main.o中还有函数被其他函数调用,那么其他.o文件会依赖于Main.o,此时Main.o是没有办法被剥离的。方案三适用于main()函数放在一个单独的cpp中的情况。

方案三的特点:

1.    消除了测试的高Mock性,一定程度上减轻了测试负担

2.    测试代码完全独立于开发代码,保持了开发代码的纯洁性,完全通过makefile控制

3.    要求产品代码的main()函数独自存在一个cpp中

 

综上所述,其实每种解决方案都有自己的优劣势,开发人员需要根据自己的项目情况来选择合适的解决方案。本文给出的只是整体的架构方案,在实际的测试过程中还会遇到各种各样的问题。如果采用方案二或方案三,开发人员在代码设计的过程中还需要更多的考虑可测性的问题。在下篇“单元测试设计中”会着重讨论如何考虑产品代码设计的可测性。本篇文章只是阐述本人在开发和测试过程中一点点经验和考虑,可能您会有更好的设计方案或者框架,欢迎一起探讨。

1 0
原创粉丝点击