APP下载

测试驱动开发框架CxxTest原理分析

2011-07-04艾智杰

科技传播 2011年20期
关键词:调用队列代码

艾智杰

同济大学电子信息与工程学院计算机应用技术系,上海 201804

1 测试驱动开发简介

测试驱动开发(TDD)是一种基于循环开发的软件开发过程。遵循TDD的编程人员,在正式进行开发之前,通常先要确定在本阶段需要实现的改进或者新功能,然后通过编写一系列的测试代码来检验这些改进和功能。一般情况下,这些测试代码都会运行失败。接下去的任务便是编写能够使得这些测试通过的代码,并且在完全通过测试后,重构代码,以达到生产标准。这个过程将会一直循环下去,直到所有的改进或者功能完成。下图展示了这一过程。

图1 基于TDD的开发循环

2 CxxTest简介

CxxTest是专门为C++语言所开发的TDD框架。它具有不需要RTTI,可以承载外部库,处理异常等优点。作为一种轻量级框架,CxxTest将所有的代码都仅包含在一个头文件(tdd.h)中。也就是说,CxxTest框架仅需要一个现代C++编译器就可以运行测试程序,甚至在必要时,可以通过它捕获异常和使用GUI展示。

CxxTest作为一种轻量级的测试驱动开发框架,其优点在于使用简单。我们通常使用已有的控制台测试启动程序来调用我们自己编写的测试用DLL。之后,该测试程序就会对此DLL的各个注册方法进行测试,并且最终输出结果。

3 CxxTest原理分析

3.1 测试过程

整个测试的过程大致可以分成两个部分,第一部分是测试类的选取,而第二部分则是具体的对我们所定义的方法的测试。图1表示的是在测试类级别上的选择,而图2则是图1中带有“*”标记步骤的具体拓展,表现了CxxTest测试驱动开发框架如何逐个调用测试类中的各个测试方法。为了让示意图尽可能简介,这里没有显示出异常处理。笔者将会另辟一节叙述。

图2 类的选取过程

图3 方法的测试过程

3.2 类和方法的注册

测试类和方法的包装注册是整个测试开始前的准备工作。这一步的注册将会告诉CxxTest框架,有哪些类、其中的哪些方法需要进行测试。

整个注册过程的第一阶段是在编译阶段通过CxxTest框架自定义的宏将所有的类对象定义为全局变量。然后当系统载入我们编写的带有测试类和方法的DLL时,首先会对全局变量进行初始化,将所有这些经过特殊处理的测试类对象加入到队列中,以供后续使用。

测试类的包装注册是通过TESTCLASS(CSomeClass)宏实现的。该宏最关键的代码如下所示:

该宏首先定义了函数CSomeClass _TddNamespaceResolv er::GetNameSpace() (未在上面的代码中展示该函数细节),用于获取CSomeClass的带有命名空间的全称,随后,通过将TDD::ClassRegistrar< CSomeClass >类的匿名对象地址加入到全局智能指针中予以保留。

这里起到关键作用的是TDD::ClassRegistrar类。在这里,我们只是使用了其构造函数。由于该类继承自TDD::ClassRegistrarBase类,所以在执行自身的构造函数之前,将会首先执行TDD::ClassRegistrarBase类的构造函数,而查看代码可知,该构造函数的核心是调用了TDD::ClassRegistrarBase::AddCla ss()方法,该方法便是初始化测试类队列,并且将各个测试类添加至队列尾的真正执行者。

最后,必须指出的是,我们真正添加进全局队列的并不是CSomeClass类对象,而是经过包装的TDD::ClassRegistrar类对象,这个对象将会在其内部产生CSomeClass对象(通过TDD::ClassRegistrar< CSomeClass >::GetInstance()方法),并且适时地调用CSomeClass的相关方法,同时也通过其构造函数存储了CSomeClass类的包含了命名空间的类全名。

第二阶段则是对测试类方法的注册。这项功能是通过TESTMETHOD(MethodName)宏实现的。其核心代码如下(略去次要部分)。

这里着重解释真正做测试类注册工作的__m_ MethodName _variable,该变量在类对象的初始化过程中、类的构造函数被触发前先被初始化。

仔细观察该变量,他属于TDD::MethodRegistrar型,其中T2(即源码中的&MethodRegistrar_ MethodName _Wrapper)作为非类型模板参数被传递,框架将会通过这个方法来间接调用我们自定义函数的。关于MethodRegistrar类的与此有关的关键代码如下,

可见,其构造函数仅仅是将测试方法加入队列,而当调用MethodRegistrar::RunTest()时,便会真正开始进行测试。

3.3 对方法进行测试

在初始化之后,程序便进入了入口点函数TDD::UnitTestBase::RunTests()。该函数其实异常简单,只是从队列中找到测试类,然后再对每一测试类找到需要测试的方法,调用多态方法MethodRegistrar:: RunClassTests ()进行测试,然后寻找下一个测试类,循环如此过程。

MethodRegistrar:: RunClassTests ()的主要经过正如“测试过程”一节中的图2所示,具体对应的函数也可以通过描述简单匹配,这里就不再赘述了。至于如何由此函数调用方法测试的执行者MethodRegistrar::RunTest(),再由此函数调用TESTMETHOD()宏所定义的包装函数,最后再回到我们自己的函数的过程,笔者将会在下一节展示。

3.4 异常处理

CxxTest的设计初衷就是为程序员提供测试框架,以检查可能的错误。为了一方面检查错误,另一方面在检查到错误之后让程序继续执行以运行更多测试来检查其他可能的错误,CxxTest的设计者对经典的C++异常机制进行了包装。

CxxTest使用了“模板方法”设计模式,将所有的异常机制都封装在TryCatch类中,该类的模板方法便是TryCatch::Execute(),在基类中,设计者将其设计为纯虚函数,以后每当需要进行测试时,都会重新定义一个类(比如说用于做方法测试的TryCatchTest类),该类继承自TryCatch类,并且重新实现Execute()函数。最终在测试时,框架则会调用

TryCatch:: TryCatchAndReport()函数,该函数的代码如下所示(略去次要代码)。

那么CxxTest又是如何重定义Execute()函数呢?其实,做法很简单,他只是简单地将Execute()函数定义为对MethodRegistrar::RunTest()的调用,该函数内部又调用了在方法注册时使用的那个测试方法的包装函数,然后由该包装函数直接调用我们所定义的测试函数(就是在TESTMETHOD()宏后面的代码)。

再深一步,根据前面的分析,框架设计者认为,应该在Execute()函数中可能会抛出异常,而该函数实际上最终调用的是我们自己所定义的代码,那我们自己的代码一定需要定义异常嘛?其实不然,我们完全可以利用CxxTest框架所提供的验证宏。这里我们仅针对最为常用的TDD_VERIFY(expression)宏进行展开分析,其他类似。该宏的关键如下所示:

其实他就是先判断expression的真假,然后直接调用TDD::Verifier::Verify()函数,此函数的功能非常简单,就是判断__tdd_b是否为假,如果为假,则抛出异常。关键代码如下:

4 结论

CxxTest作为一款轻量级的TDD框架,在设计的时候充分利用了C++的各种特性,使得其运作机制看似复杂却条例清晰。本文理出了整个CxxTest框架的运行主线,并且对其中较为重要的部分做出了详细的解释。

[1]Robert C.Martin著.敏捷软件开发:原则,模式与实践[M].邓辉,等译.清华大学出版社,2003,9.

[2]Test-driven development.http://en.wikipedia.org/wiki/Test-driven_development.14 January 2010.

[3]李瑛,彭军.测试驱动开发在系统中的设计实现及效能分析[J].计算机与数字工程,2007,35(1).

猜你喜欢

调用队列代码
队列里的小秘密
核电项目物项调用管理的应用研究
在队列里
LabWindows/CVI下基于ActiveX技术的Excel调用
创世代码
创世代码
创世代码
创世代码
丰田加速驶入自动驾驶队列
基于系统调用的恶意软件检测技术研究