CppUnit 介绍

某种意义上说,CppUnit的代码并不是很好的C++代码。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人。

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

I. 目的

前一段时间拿到公司新的引擎,发现里面使用了CppUnit来做单元测试,于是小小的研究了一把,顺便把阅读CppUnit代码的心得写下,和大家分享。某种意义上说,CppUnit的代码并不是很好的C++代码,这主要是由历史原因引起的:首先CppUnit是对JUnit的一个移植,所以很多地方是把C++作为Java在用,从风格上到语义上,都是Java的;当然这也无可厚非,因为对于移植来说,只要能够做到和原先一样用起来,别的都是第二位的。其次,CppUnit(过度的?)考虑了兼容性,为了在不同的C++编译器和标准库下都能使用,它给自己制定了很多编码标准,譬如说"不要使用mutable"、"不要使用typename"、甚至"不要使用STL容器中的at成员",...。正因为它不是很好的C++代码,并且代码量不是很大(主库80K),所以我觉得比较适合想大量使用CppUnit并且需要深入了解的人或是初步涉足C++,想阅读一些简单的源代码/库的人(如果你很有经验,想学习最新的C++技术,或是想阅读大型的库,那么你可以阅读别的开源库。)

这篇文章不适合于从未使用过CppUnit的人,如果你从未使用过CppUnit,但是对于测试驱动开发很感兴趣,可以参阅我的另一篇文章:CppUnit入门。

II. 代码

CppUnit作为一个UnitTest框架,它的主要机制就是:让你为每一段代码写出测试用例,并且把大量的测试用例按照某种方式管理起来,并且提供给你不同的界面以及输出方式。所以我觉得它的代码也可以分为相应的部分:
1.测试用例及其管理,这部分只是管理和测试相关的数据结构,它们往往是静态的初始化的,在真正的测试还没有进行前就已经完全执行完毕了。
2.实际进行测试的代码,这部分包含了测试的初始化,测试中的异常处理以及测试进程和结果的通知等。这部分代码是你的测试运行中所执行的代码。
3.测试的界面和输入。譬如说,CppUnit提供了一个基于MFC的测试界面,你可以选择一些测试用例进行测试,并且获得结果。这部分代码我们将不作详细介绍,因为和测试本身其实关系不大。

III. 测试用例及其管理

这部分的类有Test、TestLeaf、TestCase、TestComposite和TestSuite以及一些像TestPath之类的辅助类。这一部分代码其实和UnitTest本身毫无关系,它只是一个对象树的管理。CppUnit把测试系统中所有可以用来测试的对象作为一棵树进行管理,每一个测试都是树上的一个节点,这些节点都是以Test为父类的。

1. TestPath

如果把整个测试用例树比作为文件树的话,TestPath表示的是一个相对/绝对路径。它内部保存的是一个Test的队列,也就是说,它把一个类似于/root/suite/case或者是/suite/child/case的路径变为一个路径上所经过的Test对象的队列。通过这个结构,我可以很方便的在树结构中的节点间随意移动。

2. Test

Test类中,除了虚析构函数和virtual void run( TestResult *result )=0 这个纯虚函数用来执行测试以外,别的函数都是用来做树管理的。因为树是递归定义的,作为一个节点,它只需要能够枚举自身的子节点就可以了。这些相关的函数为:countTestCases用来返回这个节点及其子节点中包含了多少个有效的Test对象,这样做主要是处于计数的目的,因为Test树上很多节点本身并不包含任何测试代码;getChildTestCount返回直接的子节点个数;getChildTestAt接受一个索引,返回相应的子节点;getName返回这个节点的名字。上述函数都是纯虚函数,由子类给出具体定义。
getChildTestAt是一个虚函数,它内部实际调用的是checkIsValidIndex和doGetChildTestAt,并且CppUnit的设计者也建议你不要重载它,而是重载doGetChildTestAt函数。这个是GoF里面的Template Method模式,不过既然这里不建议重载,就不应该使用virtual函数。或者和可能是JUnit的影子。
Test类真正实现的功能是对非直接子节点的处理。如前所述,对于直接子节点可以通过一个index来表示,而对于非直接子节点则是通过TestPath来表示的。Test提供了findTestPath、findTest以及resolveTestPath函数来对这些功能提供支持。虽然这些函数是以虚函数的形式提出的,也就是说理论上可以进行扩展,但是事实上在整个CppUnit体系中,只有Test类对它们作了实现。

3. TestLeaf

TestLeaf是Test的子类,它代表了Test树上的一个叶节点。它的实现很简单,对于countTestCases返回1,因为它自身就是一个有效的Test;getChildTestCount则返回0,因为没有子节点;它的doGetChildTestAt在正常情况下根本不应该被调用。

4. TestComposite

TestComposite也是Test的子类,它代表了Test树上的一个非根节点,这里对它的使用类似于一个GoF中Composite模式。它的countTestCases返回的是对所有子节点的countTestCases的累加,从这点上可以看出,TestComposite本身只是一个Test的容器,不作为一个有效的Test。TestComposite的run函数会调用自身的doStartSuite函数,在这个函数中,会对TestResult的startSuite进行调用;然后run函数再通过doRunChildTests函数间接调用所有子节点的run,最后通过doEndSuite函数间接调用TestResult的endSuite函数。TestComposite的run函数其实也是一个Template Method模式,因为通常希望通过这三个do****函数来对其进行定制。对于TestRunner::startSuite/endSuite的调用是为了让传入的TestRunner在每个Suite被测试的前后都得到通知,以做一些簿记工作。(当然窃以为这里的名字最好叫startComposite/endComposite。)

5. TestCase

TestCase继承自TestLeaf和TestFixture,是整个CppUnit中最常用最重要的类之一。顾名思义,TestCase就代表了通常UnitTest中所指的测试用例,也是整个Test树中最常用的叶节点。这个类既在Test树中有它的位置,也直接的参与测试的进行,所以被放在下一节介绍。

6. TestSuite

TestSuite是从TestComposite继承而来的,TestComposite为那些非根节点提供了运行机制,而TestSuite则在此基础上提供了对于子节点的管理。譬如说,通过addTest函数可以加入Test,deleteContents删除所有的子Test对象,它还实现了getChildTestCount和doGetChildTestAt函数以返回子节点的个数和指针。

IV. 实际进行测试的代码

这些类包括TestFixture、TestCase、TestCaller、TestRunner、TestListener、TestResult和Protector类体系。

1. TestFixture
与其说Test类是所有Test的根,不如说TestFixture是CppUnit中所有"测试"的根。因为仅仅从Test类继承而来的类只是这个体系的"管理部门",而从TestFixture继承而来的类,才是真正进行测试的"执行部门"。TestFixture除了一个虚析构函数(C++中"请从我派生"的代名词)以外,就定义了两个虚函数:setUp和tearDown,前者初始化一次测试,后者清除一次测试所产生的所有副作用。这三个函数在TestFixture中的定义都是空的。

2. TestCase
这是我们第二次看到它了,因为它处于两个类体系的交汇点,使用多重继承从TestLeaf和TestFixture继承。这个类既没有对TestLeaf中对于作为Test树子节点方面的功能进行加强,也没有重新定义TestFixture中为空的setUp和tearDown。唯一做的就是定义了run。它的run里面,会先调用TestRunner::startCase,然后调用setUp,接着调用TestCase中新定义的runTest函数,接着调用tearDown和TestRunner::endCase。从注释中可以看出作者希望把runTest作为一个纯虚函数定义,也就是说,其实你可以从TestCase派生,并且定义一个自己的runTest函数,以进行一些简单/单一的测试工作。如果要进行复杂的测试工作/构建复杂的测试用例树,那么应该使用别的机制。这样说的原因有两方面:首先从管理角度说,如果你有几项测试任务,就应该把它们作为Test树中的不同节点,这样你可以对总任务数/失败数进行统计,而TestCase是一个TestLeaf,如果你把它们堆砌在一个TestCase中,不利于管理;其次,如果你有几项测试任务要做,并且共享同样的初始化/清除代码,那么你想从TestCase派生来做这件事情,就必须重写setUp、tearDown和run。重写前两者也算了,要是连run也重写了,这,我干嘛还从TestCase派生?

3. TestCaller
TestCaller是一个GoF中的Adapter模式,它可以把任意一个定义了setUp/tearDown的类的对象包装为一个TestCase,并且在runTest中对这个对象的某个函数进行调用。也就是说,如果我有一个TestFixture的派生类FooBarTest,其中有一个fooTest和一个barTest函数,那么TestCaller (FooBarTest::fooTest)以及TestCaller (FooBarTest::batTest)就是两个TestCase派生类,它们的实例可以作为TestCase被加入到Test树中,也可以独立的进行测试运行。
TestCaller从TestCase派生而来,并且接受某个类的实例(也可以自己通过new生成一个)以及那个方法的指针作为构造函数参数并且保存在内部,在setUp和tearDown函数中,它调用了那个对象的setUp和tearDown,并且在runTest函数中使用保存的对象对它的指定的成员函数进行调用。使用TestCaller的好处是,你可以把一组相关的Test任务放在某个从TestFixture派生而来的类中,并且用TestCaller把它们包装成若干个TestCase。这样一来便于对相关的测试任务的管理,二来也能让不同的任务成为Test树的不同子节点。

4. TestListener
TestListener其实是一个测试事件的接收器,它定义了startTest、endTest、startSuite、endSuite、startTestRun、endTestRun和addFailure方法,这些都是空的虚函数,你可以定义自己的派生类并且对自己感兴趣的事件进行处理。

5. TestResult
TestResult是从SynchronizedObject继承而来的,SynchronizedObject其实就是对于Java中synchronized的模拟,SynchronizedObject和SynchronizedObject::ExclusiveZone是一个典型的用RAII来对某一个作用域进行互斥访问的例子。TestResult定义了addListener和removeListener方法来管理事件的订阅者,并且它也定义了所有Listener的方法,当你对TestResult的某个事件方法调用时,它会把这个事件发送给所有的Listener,不过CppUnit的开发者并不认为TestResult是一个TestListener,所以即使它同相同的方法实现了TestListener的所有函数,也没有从TestListener继承。
TestResult内部维护了一个测试过程是否被强行中止的标志,并且通过reset、stop和shouldStop对其进行管理,这给予运行中的测试一个响应强行中止的机会。
TestResult增加了几个函数,runTest就是其中之一,它接收一个Test的指针作为参数,并且调用这个Test对象的run。当你有一个Test对象和一个TestResult对象的时候,你可以通过Test::run(TestResult*)或者TestResult::runTest(Test*)来完成一次测试任务,区别在于后者在调用Test::run的前后会对TestResult::startTestRun和TestResult::endTestRun进行调用。
另一些比较有趣的函数是protect、pushProtector和popProtector。这三个函数其实是维护了一个ProtectorChain对象,在protect中调用了ProtectorChain::protect来为测试提供一个受保护的环境。

6.  Protector类体系
大家都知道,我们运行某个测试任务时,可以根据返回值来判断成功还是失败,可是有些"不良"函数会抛出异常,我们必须对异常进行捕捉,否则就不能进入下一个测试而会提前用一种极其可悲的方式结束。基本的Protector只提供了一些报错的辅助函数。通过查看DefaultProtector::protect的代码,可以得知在它的protect中,会尝试捕获Exception和std::exception,并且对所有未知的异常用...进行捕捉。DefaultProtector适合作为一个"最后的选择"来使用,因为通常我们希望知道我们是否抛出了某个或者多个特定的异常,这需要使用ProtectorChain。ProtectorChain::protect中有这样一段代码:

Functors functors;
for ( int index = m_protectors.size()-1; index >= 0; --index )
{
 const Functor &protectedFunctor = functors.empty() ? functor : *functors.back();
 functors.push_back( new ProtectFunctor( m_protectors[index], protectedFunctor, context ) );
}

这段代码中,m_protectors是一组Protector,这段代码通过ProtectFunctor来把这些Protector连接在一起,并且它的根是functor,也就是那个需要保护的函数。最后对这个functors最后一个元素调用的时候,它先建立自己的保护机制,然后调用它的functor,那个functor就是它的前一个元素,可能是另一个Protector,也可能是最原始的受保护protector,这样一来,也就是说,所有的保护被一层一层的嵌套起来。

V. 小结

其实整个CppUnit中还有不少别的有意义的代码,譬如说它的测试结果输出机制,它的那些辅助宏和TestSuite的全局注册机制等,我将在以后的文章中介绍。



输入您的搜索字词 提交搜索表单

你可能感兴趣的:(C++,exception,JUnit,测试,任务,functor)