蜗牛档案室
http://jeebook.com/blog
名称 | CppUnit源码解读 |
作者 | 晨光(Morning) |
简介 | 本教程整理自站长的CppUnit源码阅读笔记,CppUnit是自动化单元测试框架的c++实现版本。如何将诸多技术综合运用到一个实际的框架中来,CppUnit为我们提供了一个难易适中的参考范例。在这里,我们可以看到STL、Design Pattern的灵活运用。希望可以通过站长的讲解,使大家能够从中汲取有益的营养。 |
声明 | 本教程版权为晨光(Morning)所有,未经允许,请勿复制、传播,谢谢。(http://morningspace.51.net/) |
目录 1 序言 2 核心部分(Core) 3 输出部分(Output) 4 辅助部分(Helper) 5 扩展部分(Extension) 6 兼听者部分(Listener) 7 界面部分(TextUI) 8 移植(Portability) 9 附录(Appendix) |
序言
[引言] [CppUnit的简单身世] [CppUnit的总体构成] [几点说明] [Test] [TestFixture] [TestCase] [TestSuite]
输出部分(Output)——基础部件
这一部分主要提供了一些用于输出测试结果的工具类,输出的方式可以有多种,比如:以纯文本方式输出,以XML标记语言方式输出,基于IDE开发环境的输出等。由此足见,CppUnit的实现者想得还是很周到的。
[Outputter] [TestResultCollector]
辅助部分(Helper)——创建机制
这一部分提供了一些辅助类,多数与创建Test类的实例有关,其中包括用于创建Test的工厂类,用于管理工厂类的注册类,可以单独运行某个测试的TestCaller,还有为方便使用而定义的一组宏。
[TypeInfoHelper] [TestFactory] [TestFactoryRegistry,NamedRegistries] [TestSuiteFactory] [TestSuiteBuilder] [TestCaller] [AutoRegisterSuite]
扩展部分(Extension)
在CppUnit中,除了提供基本的单元测试之外,还增加了很多扩展测试,比如:重复测试(RepeatedTest),正规测试(OrthodoxTest),这些内容都悉数收录在extension中。
[TestDecorator] [RepeatedTest] [Orthodox] [TestSetUp]
兼听者部分(Listener)
这部分较为简单,主要根据具体需求,提供了两个TestListener的派生类,它们分别用在不同的场合。
[TestSucessListener] [TextTestProgressListener] [TextTestResult]
界面部分(TextUI)
这一部分主要提供了一个文本界面的测试运行环境(即以字符流方式输出到标准输出设备)。该测试环境在CppUnit中被称为test runner,对应的类是TestRunner。TestRunner可以运行所有测试,或者是其中的一个。下面的代码演示了如何使用TestRunner:
CppUnit::TextUi::TestRunner runner; runner.addTest( ExampleTestCase::suite() ); runner.run( "", true ); // 空字串""代表运行所有的测试
在测试执行期间,TestRunner除了能输出最后的统计结果外,还可以打印输出跟踪信息。其中,跟踪信息的输出使用了TextTestProgressListener(见listener部分),统计结果的输出则使用了TextOutputter(见outputter部分)。当然,这些都是可选的。你可以在构造TestRunner期间或者随后通过调用setOutputter函数来指定其他类型的outputter。你也可以通过在eventManager()中注册其他TestListener,来定制跟踪信息。且看下面的示例:
CppUnit::TextUi::TestRunner runner; runner.addTest( ExampleTestCase::suite() ); // 用CompilerOutputter代替TextOutputter runner.setOutputter( CppUnit::CompilerOutputter::defaultOutputter( &runner.result(), std::cerr ) ); // 添加自定义的MyCustomProgressTestListener MyCustomProgressTestListener progress; runner.eventManager().addListener( &progress ); runner.run( "", true ); // 空字串""代表运行所有的测试
最后,TestRunner管理着其下所有测试对象的生命周期。
输出部分(Portability)
这一部分,通过若干参数的设定,解决了向不同平台移植时遇到的问题。另外还有一个叫做OStringStream的辅助类,不过morning以为,该类似乎置于helper部分更为合适。
附录(Appendix)——WIN32平台安装说明
目前,CPPUnit在WIN32平台下仅支持Microsoft Visual C++,而且你的VC++编译器至少应该是6.0版本的。
使用GUI TestRunner编译运行示例程序的步骤如下:
- 在VC++中打开examples/examples.dsw(包含所有的示例)
- 将HostApp设为active project
- 编译之
- 在VC中选择Tools/Customize.../Add-ins and Macro Files,点击Browse...
- 选择lib/TestRunnerDSPlugIn.dll文件,并按ok以注册该附加件(add-ins)
- 运行project
[Project创建结果]
框架 & 工具:
- cppunit(cppunit.lib):单元测试的框架库,你将用它来编写单元测试。
- cppunit_dll(cppunit_dll.dll/lib):同上,只是以DLL方式呈现。
- TestRunner(testrunner.dll):一个MFC的扩展DLL,用来以GUI方式运行单元测试和查看结果。
- DSPlugIn(lib/TestRunnerDSPlugIn.dll):一个VC++的附加件,为testrunner.dll所使用。有了它之后,你若在MFC TestRunner中双击某个failure,就会启动VC++,打开failure所在文件并定位到某行。
- TestPlugInRunner:(警告:实验性的)一个VC++应用程序,用以运行测试插件。测试插件就是一个公开特定接口的DLL。该应用程序目前尚未完成(auto-reload特性丢失)。
所有库文件都被置于lib/目录下。
[示例]
- CppUnitTestMain:一个实际的测试包(test suite)用来测试CppUnit。使用了TextTestRunner(文本方式的单元测试环境),利用CompilterOutputter进行post-build testing(即在编译结束之后紧跟着进行测试)。在配置中设定连接了cppunit的静态库和动态库。
- CppUnitTestApp:包含了与CppUnitTestMain相同的测试包,但使用了MFC TestRunner(GUI方式的单元测试环境)
- hierarchy : 一个演示如何子类化测试的例子(你也许更愿意使用HelperMacros.h以及宏CPPUNIT_TEST_SUB_SUITE,这种方式更为简洁清晰。本示例已经很久没有更新了)。
- HostApp : 一个用MFC TestRunner演示各种失败测试的例子。也演示了MFC Unicode TestRunner。
- TestPlugIn : 一个演示如何为TestPlugInRunner编写TestPlugIn的例子(实验性的).
[配置(Configuration)]
CppUnit和TestRunner带有3种配置。
- Release():多线程DLL,release模式
- Debug(d):Debug多线程DLL,debug模式
- Unicode Release(u):Unicode多线程DLL,release模式
- Unicode Debug(ud):Unicode Debug 多线程DLL,debug模式
- Debug Crossplatform (cd): Debug 多线程DLL,没有使用type_info从类名中提取测试用例的包名。
对CppUnit而言,当创建dll时,字母“dll” 将被添加到后缀之后。
括号内的字母标明了添加到库名之后的后缀。例如,debug配置的cppunit静态库名为cppunitd.lib。debug配置的cppunit动态库名为cppunitd_dll.lib.
[创建(Building)]
- 在VC++中打开src/CppUnitLibraries.dsw工作区文件。
- 将TestPlugInRunner设为active project。
- 在'Build'菜单中选择'Batch Build...'
- 在Batch Build对话框中,选中所有的project 并按下build按钮。
- 所有的库文件可以在lib/目录下找到。
[测试(Testing)]
- 打开工作区文件examples/Examples.dsw。
- 将CppUnitTestApp设为active project.
- 为你要创建的库选择合适的配置。
- 编译运行project。TestRunner GUI将会出现。
[库(Libraries)]
所有编译后生成的库均可在'lib'目录中找到。多数库可以在src/CppUnitLibraries.dsw工作区中创建。
lib/:
- cppunit.lib : CppUnit静态库“Multithreaded DLL”
- cppunitd.lib : CppUnit静态库“Debug Multithreaded DLL”
- cppunit_dll.dll : CppUnit动态库(DLL)“Multithreaded DLL”
- cppunit_dll.lib : CppUnit动态导入库“Multithreaded DLL”
- cppunitd_dll.dll : CppUnit动态库(DLL)“Debug Multithreaded DLL”
- cppunitd_dll.lib : CppUnit动态导入库“Debug Multithreaded DLL”
- qttestrunner.dll : QT TestRunner动态库(DLL)“Multithreaded DLL”
- qttestrunner.lib : QT TestRunner导入库“Multithreaded DLL”
- testrunner.dll : MFC TestRunner动态库(DLL)“Multithreaded DLL”
- testrunner.lib : MFC TestRunner导入库“Multithreaded DLL”
- testrunnerd.dll : MFC TestRunner动态库(DLL)“Debug Multithreaded DLL”
- testrunnerd.lib : MFC TestRunner导入库“Debug Multithreaded DLL”
- testrunneru.dll : MFC Unicode TestRunner动态库(DLL)“Multithreaded DLL”
- testrunneru.lib : MFC Unicode TestRunner导入库“Multithreaded DLL”
- testrunnerud.dll : MFC Unicode TestRunner动态库(DLL)“Debug Multithreaded DLL”
- testrunnerud.lib : MFC Unicode TestRunner导入库“Debug Multithreaded DLL”
- TestRunnerDSPlugIn.dll : 注册到你的VC++中的附加件。
注意:当你使用CppUnit DLL(cppunit*_dll.dll)时,你必须连接相关的导入库,并在project中定义预处理标识CPPUNIT_DLL。
[使用CppUnit]
- 编写单元测试:
为了编写单元测试,你需要连接cppunitXX.lib,此处的XX即所选配置对应的后缀字母。 你必须在你的project中打开RTTI开关(Project Settings/C++/C++ Language)。 CppUnit的include目录必须包含在include查找路径中。你可以通过在Project Settings/C++/Preprocessor/Additional include directories或者Tools/Options/Directories/Include中添加include目录做到这一点。
简言之:
- 打开RTTI开关
- 连接lib/cppunitXX.lib
- include/ 必须包含在include查找路径中
- 使用TestRunner GUI:
为了使用GUI的test runner,你需要连接testrunnerXX.lib和cppunitXX.lib,此处的XX即所选配置对应的后缀字母。 你必须在你的project中打开RTTI开关。 文件testrunner.dll必须位于你的应用程序所在的路径(Debug或Release目录,project的dsp文件所在目录,或环境变量PATH中所指定的目录)。 一个最简单的办法是,要么添加一个post-build命令,或者,将位于lib/目录下的testrunner.dll添加到你的project中来,并定制创建步骤,将dll文件拷贝到你的“中间结果”目录(通常是Debug或Release目录)。
因为TestRunner GUI是一个MFC的扩展DLL,它能够访问当前应用程序的CWinApp。 参数设置将使用应用程序的注册键。这意味着,设置项“最近使用的测试”对每个应用程序而言都是不同的。
简言之:
- 打开RTTI开关
- 连接lib/cppunitXX.lib和lib/testrunnerXX.lib
- include/必须包含在include查找路径中
- 为了运行你的project,lib/testrunnerXX.dll必须可用
- 使用DSPlugIn:
你必须在VC++中注册该插件。在Tools/Customize/Add-ins and Macro files中点击browse,并选择lib/TestRunnerDSPlugIn.dll(你可以注册release版或者debug版,都能运行)。
若VC++正在运行,当你双击一个failure后,VC++将打开相关文件并定位到出错行。
- 使用Test Plug In Runner:
你的DLL必须导出(export)一个函数,该函数实现了在include/msvc6/testrunner/TestPlugInInterface.h中所定义的接口。作为范例,参见examples/msvc6/TestPlugIn/TestPlugInInterfaceImpl.*。注意:该runner仍处于实验阶段并未作足够多的测试。
相关文件:Portablility.h
其实OStringStream在先前很多地方都曾经出现过,比如:TestFactoryRegistry中、XmlOutputter中、TestAssert中。其作用是将整数转换为字符串并输出,功能上类似于C语言的itoa函数。事实上,从随CppUnit所附的ChangeLog中可以看到,先前正是用的itoa,只不过后来的一次refactoring中,才被OStringStream取代。其实现代码如下:
#if CPPUNIT_HAVE_SSTREAM # include <sstream> namespace CppUnit { class OStringStream : public std::ostringstream { }; } #else #if CPPUNIT_HAVE_CLASS_STRSTREAM # include <string> # if CPPUNIT_HAVE_STRSTREAM # include <strstream> # else # include <strstream.h> # endif namespace CppUnit { class OStringStream : public std::ostrstream { public: std::string str() { (*this) << '/0'; std::string msg(std::ostrstream::str()); std::ostrstream::freeze(false); return msg; } }; } #else # error Cannot define CppUnit::OStringStream. #endif #endif
相关文件:Portability.h,config-msvc6.h,config-bcb5.h
如前所述,CppUnit提供了一系列可供设置的参数(其实就是一系列宏定义),针对不同平台,你需要做出不同的选择。好在CppUnit的实现者为我们做了很多工作,使我们不用太多考虑这方面的问题,至少在Visual C++ 6.0和Borland C++ Builder 5.0这两个平台上是如此。Portability.h的开头有如下这样一段代码,它依据实际的语言平台(由特定的宏来指定),载入相应的参数设置文件:
#if defined(__BORLANDC__) # include <cppunit/config-bcb5.h> #elif defined (_MSC_VER) # include <cppunit/config-msvc6.h> #else # include <cppunit/config-auto.h> #endif
这里的config-auto.h文件,morning并未在CppUnit的源码中找到,也许是程序作者的一时疏忽。至于在config-xxx.h中出现的那些参数,此处只列举一二,感兴趣的读者可自己去查看源码。
// helper部分的TypeInfoHelper中曾经出现过该宏 // 它表明在bcb和vc中函数std::string::compare的调用方式不一样 // config-msvc6.h #ifdef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST #undef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST #endif // config-bcb5.h #ifndef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST #define CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST 1 #endif // 指明编译器是否支持c++的namespace语言特性 // bcb和vc均支持namespace // config-msvc6.h #ifndef CPPUNIT_HAVE_NAMESPACES #define CPPUNIT_HAVE_NAMESPACES 1 #endif // config-bcb5.h #ifndef CPPUNIT_HAVE_NAMESPACES #define CPPUNIT_HAVE_NAMESPACES 1 #endif // OStringStream的定义中曾经出现过,指明是否存在<sstream>头文件, // bcb和vc均包含有<sstream>头文件 // config-msvc6.h #define CPPUNIT_HAVE_SSTREAM 1 // config-bcb5.h #define CPPUNIT_HAVE_SSTREAM 1
此外还有一些,仅在某个平台下才会用到的参数,比如,以下内容均只在config-msvc6.h中出现:
// 忽略Debug符号大于255的警告 #if _MSC_VER > 1000 // VC++ #pragma warning( disable : 4786 ) #endif // _MSC_VER > 1000 // 若当前创建的是CppUnit的DLL库,则需要定义CPPUNIT_DLL_BUILD #ifdef CPPUNIT_BUILD_DLL #define CPPUNIT_API __declspec(dllexport) #endif // 若当前要链接到CppUnit的DLL库,则需要定义CPPUNIT_DLL #ifdef CPPUNIT_DLL #define CPPUNIT_API __declspec(dllimport) #endif
最后,Portability.h中还定义了一些平台无关的参数,它们设定同样也影响着CppUnit的某些特性,比如:
// 若你希望使用传统风格的断言宏,比如: // assert(), assertEqual()等等,则需将其设定为1 #ifndef CPPUNIT_ENABLE_NAKED_ASSERT #define CPPUNIT_ENABLE_NAKED_ASSERT 0 #endif // CPPUNIT_API是在<config_msvc6.h>中被定以的 // 若没有被定以,则表明程序不需要引用或生成CppUnit的DLL库 #ifndef CPPUNIT_API #define CPPUNIT_API #undef CPPUNIT_NEED_DLL_DECL #define CPPUNIT_NEED_DLL_DECL 0 #endif
相关文件:TestRunner.h,TestRunner.cpp,TextTestRunner.h
TestRunner中定义了4个protected属性的成员变量:
TestSuite *m_suite; // 对应待运行的测试 TestResultCollector *m_result; // 搜集测试结果(见output部分) TestResult *m_eventManager; // 收集测试过程中的相关信息 Outputter *m_outputter; // 输出测试统计结果
对于这几个成员变量的作用,注释中及前面部分已有提及。在这里,大家对TestResult和TestResultCollector的功能可能容易混淆。对于它们的区别以及TestResultCollector的“身世”,请见output部分。
TestRunner的ctor对这几个成员变量进行了初始化,若外部传入的outputter为空,则缺省创建TextOutputter对象赋给m_outputter,另外还调用了m_eventManager的addListener方法,将m_result作为一个兼听者加入其中。
TestRunner::TestRunner( Outputter *outputter ) : m_outputter( outputter ) , m_suite( new TestSuite( "All Tests" ) ) , m_result( new TestResultCollector() ) , m_eventManager( new TestResult() ) { if ( !m_outputter ) m_outputter = new TextOutputter( m_result, std::cout ); m_eventManager->addListener( m_result ); }
dtor则负责回收在ctor中所创建的资源:
TestRunner::~TestRunner() { delete m_eventManager; delete m_outputter; delete m_result; delete m_suite; }
正如前面所说,你可以在构造TestRunner之后,通过调用setOutputter函数来指定其他类型的outputter:
void TestRunner::setOutputter( Outputter *outputter ) { delete m_outputter; m_outputter = outputter; }
TestRunner中的主要接口就是run方法,通过调用该函数才能运行测试:
bool TestRunner::run( std::string testName, bool doWait, bool doPrintResult, bool doPrintProgress ) { runTestByName( testName, doPrintProgress ); printResult( doPrintResult ); wait( doWait ); return m_result->wasSuccessful(); }
其中,testName是测试用例的名称,若为空则运行所有测试,否则运行指定测试;doWait为true时,表示在run函数返回之前,用户必须按一下RETURN键才能结束;doPrintResult为true时,测试结果将被输出到标准输出设备(前提是使用TextOutputter),否则不产生任何输出;doPrintProgress为true时,测试过程将被输出到标准输出设备(前提是使用TextTestProgressListener),否则不产生任何输出。
这里调用了runTestByName,请看源码及注释:
bool TestRunner::runTestByName( std::string testName, bool doPrintProgress ) { if ( testName.empty() ) // 若testName为空则运行所有测试 return runTest( m_suite, doPrintProgress ); Test *test = findTestByName( testName ); // 否则查找指定测试 if ( test != NULL ) return runTest( test, doPrintProgress ); // 若成功找到则运行之 std::cout << "Test " << testName << " not found." << std::endl; return false; }
在runTestByName中,根据测试名称查找指定测试的任务落实到了findTestByName肩上:
Test *TestRunner::findTestByName( std::string name ) const { for ( std::vector<Test *>::const_iterator it = m_suite->getTests().begin(); it != m_suite->getTests().end(); ++it ) { Test *test = *it; if ( test->getName() == name ) return test; } return NULL; }
该函数以const_iterator遍历m_suite中的所有测试,寻找名字相符的测试。不过,从代码中可以看出,这里无法支持多层嵌套结构的测试集,这也算是一点遗憾吧。
找到指定测试之后,就可以将运行测试的任务转交给runTest方法了:
bool TestRunner::runTest( Test *test, bool doPrintProgress) { TextTestProgressListener progress; if ( doPrintProgress ) // 若doPrintProgress为true则显示测试过程 m_eventManager->addListener( &progress ); test->run( m_eventManager ); // 此处才真正运行测试,m_eventManager就是TestResult if ( doPrintProgress ) // 若doPrintProgress为true则移除先前加入的progress m_eventManager->removeListener( &progress ); return m_result->wasSuccessful(); // 返回测试结果成功与否 }
测试结束后,就可以通过调用printResult输出测试结果了:
void TestRunner::printResult( bool doPrintResult ) { std::cout << std::endl; if ( doPrintResult ) m_outputter->write(); }
另外,TestRunner还提供了一个addTest方法,用以添加测试,其内部只是简单的调用了一下m_suite的addTest方法,相当简单:
void TestRunner::addTest( Test *test ) { if ( test != NULL ) m_suite->addTest( test ); }
当然还少不了几个成员变量的getter方法:
TestResultCollector &TestRunner::result() const { return *m_result; } TestResult &TestRunner::eventManager() const { return *m_eventManager; }
另一个相关的头文件TextTestRunner.h中,仅仅简单地做了一个typedef定义:
typedef CppUnit::TextUi::TestRunner TextTestRunner;
这就是TestRunner的大致内容。
相关文件:TestSucessListener.h,TestSucessListener.cpp
派生自TestListener和SynchronizedObject(多重继承),兼具两者特性。作为一个实际的Observer,接收来自TestResult的信息,用以“监听”测试是否成功。关于TestListener、SynchronizedObject以及TestResult请见core部分的说明。
TestSucessListener内部所持有的成员变量m_sucess标示了测试成功与否,至于究竟如何“监听”,不妨来看一下TestSucessListener的相关实现:
TestSucessListener::TestSucessListener( SynchronizationObject *syncObject ) : SynchronizedObject( syncObject ) , m_sucess( true ) { }
在ctor中,m_sucess被初始化为true。至于syncObject,则提供了同步功能,为后续调用ExclusiveZone提供便利。
void TestSucessListener::addFailure( const TestFailure &failure ) { ExclusiveZone zone( m_syncObject ); m_sucess = false; }
在测试发生错误时,TestResult将会调用TestSucessListener的addFailure,后者只是简单地将m_sucess设置为false。TestSucessListener只关心测试成功与否,至于有关测试结果的详细情况则由TestResultCollector负责“监听”,关于TestResultCollector请见output部分。因为要考虑多线程环境,所以用到了ExclusiveZone,这也是为什么TestSucessListener需要继承SynchronizedObject的原因(别忘了ExclusiveZone是protected属性的)。m_syncObject就是前面ctor中提到的syncObject。
void TestSucessListener::reset() { ExclusiveZone zone( m_syncObject ); m_sucess = true; }
顾名思义,在测试运行之前reset内部状态,将m_sucess置为true。
最后,为外部提供测试成功与否的查询接口也是必不可少的:
bool TestSucessListener::wasSuccessful() const { ExclusiveZone zone( m_syncObject ); // [此处是read操作,似不必劳驾ExclusiveZone] return m_sucess; }
相关文件:TextTestProgressListener.h,TextTestProgressListener.cpp
派生自TestListener,用来“监听”测试用例的运行状态,并将结果定向到标准错误输出设备(即屏幕)。作为一个简易的文本流方式的结果输出工具,TextTestProgressListener已是绰绰有余了。
// 继承自基类的虚函数,在测试运行前被调用 void TextTestProgressListener::startTest( Test *test ) { std::cerr << "."; std::cerr.flush(); } // 继承自基类的虚函数,运行测试失败时被调用 void TextTestProgressListener::addFailure( const TestFailure &failure ) { std::cerr << ( failure.isError() ? "E" : "F" ); std::cerr.flush(); } // 在测试运行后被调用[疑为endTest,也许是作者的疏忽] void TextTestProgressListener::done() { std::cerr << std::endl; std::cerr.flush(); }
相关文件:TextTestResult.h,TextTestResult.cpp
以文本流方式输出测试运行的结果。不过该类在新版本中已被标上了“DEPRECATED”,并被TextTestProgressListener和TextOutputter(在outputter部分讲解)所取代。因为是不推荐使用的,所以此处不准备细述了,感兴趣的读者可以自己看。
相关文件:TestDecorator.h
它提供了一种方法,可以不用子类化Test类,同时又能扩展Test类的功能。我们可以派生TestDecorator,并用它来包装Test。其实这种方法是Decorator Pattern的一个应用,在GoF中对该pattern有如下描述:动态地给一个对象添加一些额外的职责。就增加功能来说,比生成子类更为灵活。
TestDecorator维护了一个指向Test实例的指针,并在ctor中设定。不过该实例的生命期,TestDecorator并不过问:
protected: Test *m_test;
随后是四个public函数,其接口与Test的接口完全一致:
void run (TestResult *result); int countTestCases () const; std::string getName () const; std::string toString () const;
函数的实现就是简单的调用m_test的对应接口:
inline int TestDecorator::countTestCases () const { return m_test->countTestCases (); } inline void TestDecorator::run (TestResult *result) { m_test->run (result); } inline std::string TestDecorator::toString () const { return m_test->toString (); } inline std::string TestDecorator::getName () const { return m_test->getName(); }
在TestDecorator的派生类中,这些功能将得到扩展。
相关文件:RepeatedTest.h,RepeatedTest.cpp
派生自TestDecorator,其功能是对测试重复运行指定的次数(类似于某种强度测试)。private成员变量m_timesRepeat记录了重复的次数:
private: const int m_timesRepeat;
该值在ctor中设定:
RepeatedTest( Test *test,int timesRepeat ) : TestDecorator( test ), m_timesRepeat(timesRepeat) {}
这里的test参数,就是所要执行的测试,可能是某个测试用例,也可能是测试包。
随后是函数countTestCases、run和toString的子类化版本:
// 返回本次测试中测试用例的总数 // 总数 = 实际总数 * 重复次数 int RepeatedTest::countTestCases() const { return TestDecorator::countTestCases () * m_timesRepeat; } std::string RepeatedTest::toString() const { return TestDecorator::toString () + " (repeated)"; } // 运行测试 // 重复调用基类的run,并在基类中调用m_test的run方法 void RepeatedTest::run( TestResult *result ) { for ( int n = 0; n < m_timesRepeat; n++ ) { if ( result->shouldStop() ) break; TestDecorator::run( result ); } }
相关文件:Orthodox.h
该类实现了正规测试的功能。它派生自TestCase,是一个模板类,有一个类型参数ClassUnderTest,代表将要运行的测试。所谓正规测试,就是对待测类(即ClassUnderTest)执行一组简单的测试,确保其至少具有如下基本操作:
- default ctor
- operator==和operator!=
- assignment(即operator=)
- operator!
- safe passage(即copy ctor)
若其中任何一项没有通过测试,则模板类就不会实例化。否则,实例化后将检查这些操作的语义是否正确。当你需要确认一组待测类具有相同表现时,采用被模板化的测试用例非常有用,Orthodox就是一个很好的例子。可以想见,在实际工作中,我们也可以效仿Orthodox的做法,从而“扩展”CppUnit以适应自己的特定环境。
以下代码演示了如何将一个复数类的正规测试添加到测试包中:
TestSuite *suiteOfTests = new TestSuite; suiteOfTests->addTest (new ComplexNumberTest ("testAdd"); suiteOfTests->addTest (new TestCaller<Orthodox<Complex> > ()); // 非常简单
来看一下Orthodox的定义:
template <typename ClassUnderTest> class Orthodox : public TestCase { public: Orthodox () : TestCase ("Orthodox") {} protected: ClassUnderTest call (ClassUnderTest object); void runTest (); };
唯一需要解释的就是runTest方法,Orthodox是如何检查ClassUnderTest是否符合要求的呢:
template <typename ClassUnderTest> void Orthodox<ClassUnderTest>::runTest () { // 确保default ctor被定义,否则无法通过编译 ClassUnderTest a, b, c; // 确保operator==被定义,否则无法通过编译 // 同时检查operator==的语义 CPPUNIT_ASSERT (a == b); // 确保operator!、operator=和operator!=被定义 // 否则无法通过编译 // 同时检查operator!=的语义 b.operator= (a.operator! ()); CPPUNIT_ASSERT (a != b); // 检查operator!和operator==的语义 b = !!a; CPPUNIT_ASSERT (a == b); b = !a; // 以下检查copy ctor是否被定义及其语义正确与否 c = a; CPPUNIT_ASSERT (c == call (a)); c = b; CPPUNIT_ASSERT (c == call (b)); }
这里的call是辅助函数,“迫使”编译器调用copy ctor,以检查safe passage:
template <typename ClassUnderTest> ClassUnderTest Orthodox<ClassUnderTest>::call (ClassUnderTest object) { return object; }
所有的奥妙就在上面这几行代码中。
相关文件:TestSetUp.h,TestSetUp.cpp
同样派生自TestDecorator,它使测试类具有了SetUp和TearDown的特性。关于这两个特性请见core部分的TestFixture。
该类定义了两个protected属性的虚函数,以供派生类覆盖:
protected: virtual void setUp(); virtual void tearDown();
此外,就是子类化了run方法:
void TestSetUp::run( TestResult *result ) { setUp(); TestDecorator::run(result); tearDown(); }
相关文件:TypeInfoHelper.h,TypeInfoHelper.cpp
为了扫清理解障碍,TypeInfoHelper是首先需要解释的。该类的作用是根据指定类的type_info返回一个代表其类名的字符串。为了使用此功能,你必须定义CPPUNIT_USE_TYPEINFO_NAME宏,即你必须确认你所使用的c++编译器提供了type_info机制。TypeInfoHelper仅有一个static成员函数getClassName,请留意morning的注释:
std::string TypeInfoHelper::getClassName( const std::type_info &info ) { static std::string classPrefix( "class " ); std::string name( info.name() ); // 调用info的name以得到类名信息 // 确定类名中是否有"class"字样 bool has_class_prefix = 0 == #if CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST name.compare( classPrefix, 0, classPrefix.length() ); #else name.compare( 0, classPrefix.length(), classPrefix ); #endif // 返回不带有"class"字样的类名 return has_class_prefix ? name.substr( classPrefix.length() ) : name; }
关于此处用到的std::string::compare函数,在bcb和vc中的调用方式不一样,所以就有了CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST宏。参见config-msvc6.h和config-bcb5.h中的相关定义以及portability部分的说明。
相关文件:TestFactory.h
是Test的抽象类工厂(Abstract Factory),用于创建一个Test实例,它仅仅包含了一个纯虚函数makeTest的声明:
virtual Test* makeTest() = 0;
相关文件:TestFactoryRegistry.cpp,TestFactoryRegistry.cpp
某次测试的运行可能包含了许多测试实例,它们彼此间可能呈现层状结构,而每个测试实例的创建都是由某个与之对应的类工厂完成的。为了较好的管理这些类工厂,实现其生命周期的自动操控,CppUnit采用了一种注册机制。类TestFactoryRegistry和类NamedRegistries就是用来实现该机制的。
NamedRegistries是一个管理类,用以管理所有的注册项——TestFactoryRegistry类的实例,由它全权负责TestFactoryRegistry的生命周期。TestFactoryRegistry在稍后会讲到。
NamedRegistries采用了Singleton Pattern,以保证其“全局性”的唯一访问点。此处是通过在函数内定义静态变量的方式来实现的:
NamedRegistries & NamedRegistries::getInstance() { static NamedRegistries namedRegistries; return namedRegistries; }
NamedRegistries内部有三个private属性的成员变量:
Registries m_registries; // 代表一个注册名称-注册项的映射表 Factories m_factoriesToDestroy; // 代表即将被销毁的注册项序列 Factories m_destroyedFactories; // 代表已经被销毁的注册项序列
其中,Registries和Factories的定义如下:
typedef std::map<std::string, TestFactoryRegistry *> Registries; typedef std::set<TestFactory *> Factories;
为了使外界可以访问到注册项,NamedRegistries提供了getRegistry方法,请留意morning的注释:
TestFactoryRegistry & NamedRegistries::getRegistry( std::string name ) { // 根据name在m_registries中查找注册项 Registries::const_iterator foundIt = m_registries.find( name ); // 若没有找到,则创建一个TestFactoryRegistry实例,并赋以name作为名称 // 将之分别插入m_registries和m_factoriesToDestroy中 // 再返回该TestFactoryRegistry实例 if ( foundIt == m_registries.end() ) { TestFactoryRegistry *factory = new TestFactoryRegistry( name ); m_registries.insert( std::make_pair( name, factory ) ); m_factoriesToDestroy.insert( factory ); return *factory; } // 若找到,则直接返回 return *foundIt->second; }
在NamedRegistries被销毁(即dtor被调用)的同时,其下所属的TestFactoryRegistry实例也将被销毁:
NamedRegistries::~NamedRegistries() { Registries::iterator it = m_registries.begin(); while ( it != m_registries.end() ) { TestFactoryRegistry *registry = (it++)->second; if ( needDestroy( registry ) ) delete registry; } }
这里加上needDestroy的判断是为了防止出现多次销毁同一个TestFactoryRegistry实例的现象,稍后可以发现这和TestFactoryRegistry的dtor实现有关,另外一个wasDestroyed方法,也与此有关,它们的实现代码分别如下:
void NamedRegistries::wasDestroyed( TestFactory *factory ) { // 从m_factoriesToDestroy中摘除factory m_factoriesToDestroy.erase( factory ); // 将factory插入m_destroyedFactories m_destroyedFactories.insert( factory ); } bool NamedRegistries::needDestroy( TestFactory *factory ) { // 判断m_destroyedFactories是否存在factory return m_destroyedFactories.count( factory ) == 0; }
根据约定,TestFactory的注册项必须调用wasDestroyed方法,以表明一个TestFactoryRegistry实例已经被成功销毁了。同时,它也需要调用needDestroy以确信一个给定的TestFactory可以被允许销毁,即事先没有被其他TestFactoryRegistry实例销毁。
我们再来看看TestFactoryRegistry。其ctor只是简单的将传入其中的字符串赋给成员变量m_name,它代表了注册项的名称:
TestFactoryRegistry::TestFactoryRegistry( std::string name ) : m_name( name ) { }
dtor稍微复杂一些,请留意morning的注释:
TestFactoryRegistry::~TestFactoryRegistry() { // 还记得前面提到的约定吗? NamedRegistries::getInstance().wasDestroyed( this ); // 遍历其下所属的各个TestFactory实例 for ( Factories::iterator it = m_factories.begin(); it != m_factories.end(); ++it ) { TestFactory *factory = it->second; // 若factory没有存在于NamedRegistries::m_destroyedFactories中 // 则可以放心销毁之。factory的销毁可能形成连锁反应,亦即,若factory本身 // 也是TestFactoryRegistry类型的,其dtor又将被调用,上述过程将再次重现 if ( NamedRegistries::getInstance().needDestroy( factory ) ) delete factory; } }
这里我们再次看到了wasDestroyed和needDestroy,正如前面所述,它们是为了防止多次销毁同一个TestFactoryRegistry实例的。对于如下的代码:
registerFactory( "All Tests", getRegistry( "Unit Tests" ) );
在了解了TestFactoryRegistry::getRegistry的实际行为之后,你会发现,名为“Unit Tests”的注册项将同时被名为"All Tests"的注册项和NamedRegistries所拥有。此外,morning以为,对于没有在NamedRegistries中注册的TestFactoryRegistry实例,调用needDestroy的结果同样为true,此处也保证可以被及时销毁,而不致造成内存泄漏。由此足见,此处的注册机制是相当灵活的。
上面出现的m_factories是TestFactoryRegistry的一个private属性的成员变量:
Factories m_factories; // 代表一个类工厂名称-类工厂实例的映射表 Factories的定义如下: typedef std::map<std::string, TestFactory *> Factories;
至于getRegistry方法,则有两个版本,一个有形参,另一个则没有:
TestFactoryRegistry & TestFactoryRegistry::getRegistry() { // 调用另一个版本的getRegistry,并传入“All Tests” // 一般代表某组测试的“根节点” return getRegistry( "All Tests" ); } TestFactoryRegistry & TestFactoryRegistry::getRegistry( const std::string &name ) { // 获取NamedRegistries的实例,并调用其getRegistry方法 return NamedRegistries::getInstance().getRegistry( name ); }
前面提到,某个类工厂的注册项,其下可能包含一组类工厂,即形成所谓的层状结构。为了支持这一功能,TestFactoryRegistry提供了registerFactory方法,同样有两个不同版本:
// 给定类工厂的名称及其对应的实例指针 void TestFactoryRegistry::registerFactory( const std::string &name, TestFactory *factory ) { m_factories[name] = factory; } // 只给出了类工厂的实例指针,此之渭Unnamed TestFactory void TestFactoryRegistry::registerFactory( TestFactory *factory ) { // 通过serialNumber自动形成名称,再调用另一个版本的registerFactory // static变量serialNumber从1开始,每次累加1 static int serialNumber = 1; OStringStream ost; ost << "@Dummy@" << serialNumber++; registerFactory( ost.str(), factory ); }
当层状结构构建好后,就可以调用makeTest方法,创建待运行的测试实例了,请留意morning的注释:
Test * TestFactoryRegistry::makeTest() { // 创建一个测试包,并冠以m_name的名称 TestSuite *suite = new TestSuite( m_name ); // 调用addTestToSuite,以将其下所属的测试实例添加到suite中 addTestToSuite( suite ); // 返回测试包对应的指针 return suite; } void TestFactoryRegistry::addTestToSuite( TestSuite *suite ) { // 遍历其下所属的各个TestFactory实例 for ( Factories::iterator it = m_factories.begin(); it != m_factories.end(); ++it ) { TestFactory *factory = (*it).second; // 调用factory的makeTest方法创建测试实例 // 将指向实例的指针添加到suite中 // makeTest的调用可能形成连锁反应 suite->addTest( factory->makeTest() ); } // 当所有的factory都遍历完后,即所有的测试实例都被创建成功后 // 整个测试实例的层状结构也就构建成功了 }
以下对CppUnit的注册机制作一个简单的小结:
- NamedRegistries以“线性”方式管理着所有的类工厂注册项——TestFactoryRegistry,负责维护其生命周期
- 一个TestFactoryRegistry对应一个类工厂实例,其下可能包含一系列的类工厂实例,构成层状结构
- TestFactory(s)可以注册到NamedRegistries中,也可以不注册,它们要么被NamedRegistries销毁,要么被其所属之TestFactoryRegistry销毁
- 所有的类工厂都注册完毕后,TestFactoryRegistry::makeTest方法的一次调用,将形成连锁反映(即递归调用),以创建一组具有层状结构的测试实例
以下提供几个使用TestFactoryRegistry类的例子,以加深认识:
// 例1:创建一个空的测试包,并将与之对应的类工厂注册项注册到NamedRegistries中 CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(); CppUnit::TestSuite *suite = registry.makeTest(); // 例2:创建一个名为“Math”的测试包,并将与之对应的类工厂注册项注册到NamedRegistries中 CppUnit::TestFactoryRegistry &mathRegistry = CppUnit::TestFactoryRegistry::getRegistry( "Math" ); CppUnit::TestSuite *mathSuite = mathRegistry.makeTest(); // 例3:创建一个名为“All tests”的测试包,并将名为“Graph”和“Math”的测试包作为“All tests”测试包的子项 // 与全部三个测试包对应的类工厂注册项都被注册到NamedRegistries中 CppUnit::TestSuite *rootSuite = new CppUnit::TestSuite( "All tests" ); rootSuite->addTest( CppUnit::TestFactoryRegistry::getRegistry( "Graph" ).makeTest() ); rootSuite->addTest( CppUnit::TestFactoryRegistry::getRegistry( "Math" ).makeTest() ); CppUnit::TestFactoryRegistry::getRegistry().addTestToSuite( rootSuite ); // 例4:例3的另一中实现方式 CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(); registry.registerFactory( CppUnit::TestFactoryRegistry::getRegistry( "Graph" ) ); registry.registerFactory( CppUnit::TestFactoryRegistry::getRegistry( "Math" ) ); CppUnit::TestSuite *suite = registry.makeTest();
相关文件:TestSuiteFactory.h
模板类,派生自TestFactory,是TestFixture的类工厂,并且该TestFixture必须实现一个静态的suite方法,以便在覆盖TestFactory的makeTest时调用:
// 此处的TestCaseType就是一个TestFixture template<typename TestCaseType> class TestSuiteFactory : public TestFactory { public: virtual Test *makeTest() { return TestCaseType::suite(); } };
相关文件:TestSuiteBuilder.h
模板类,用以将一系列测试添加到一个测试包中。所有加入该测试包的测试,其固有名称之前都会被加上测试包的名称,形成类似如下的测试名称:MyTestSuiteName.myTestName,前者为测试包的名称,后者为测试本身的名称。
TestSuiteBuilder内部维护了一个m_suite指针以指向对应的测试包实例,这是一个Smart Pointer,因此其生命周期无需手工操控,而是由TestSuiteBuilder来维护:
std::auto_ptr<TestSuite> m_suite;
至于m_suite所指的对象,可以由TestSuiteBuilder自己创建,也可以从外面传入,全凭你选择调用ctor的哪个版本了:
// 使用type_info生成测试包的名称 #if CPPUNIT_USE_TYPEINFO_NAME TestSuiteBuilder() : m_suite( new TestSuite( TypeInfoHelper::getClassName( typeid(Fixture) ) ) ) { } #endif TestSuiteBuilder( TestSuite *suite ) : m_suite( suite ) { } TestSuiteBuilder(std::string name) : m_suite( new TestSuite(name) ) { }
添加测试的方法是简单地调用m_suite的addTest:
void addTest( Test *test ) { m_suite->addTest( test ); }
此外,为了方便使用,TestSuiteBuilder还提供了几个用于添加TestCaller的方法,它们调用makeTestName以生成测试名称,最终都将调用addTest。其中,Fixture是TestSuiteBuilder的模板类型参数,TestMethod的定义如下:
typedef void (Fixture::*TestMethod)();
至于TestCaller,稍后会讲到:
void addTestCaller(std::string methodName, TestMethod testMethod ) { Test *test = new TestCaller<Fixture>( makeTestName( methodName ), testMethod ); addTest( test ); } void addTestCaller(std::string methodName, TestMethod testMethod, Fixture *fixture ) { Test *test = new TestCaller<Fixture>( makeTestName( methodName ), testMethod, fixture); addTest( test ); } template<typename ExceptionType> void addTestCallerForException(std::string methodName, TestMethod testMethod, Fixture *fixture, ExceptionType *dummyPointer ) // dummyPointer本身没有实际作用,此处只为获取其所属类型 { Test *test = new TestCaller<Fixture,ExceptionType>( makeTestName( methodName ), testMethod, fixture); addTest( test ); }
makeTestName的定义如下:
std::string makeTestName( const std::string &methodName ) { return m_suite->getName() + "." + methodName; }
为了便于外界访问m_suite指针,TestSuiteBuilder还提供了如下辅助方法:
TestSuite *suite() const { return m_suite.get(); } TestSuite *takeSuite() { return m_suite.release(); }
相关文件:TestCaller.h
在前面以及core部分曾经多次提到TestCaller,此类的作用是根据一个fixture创建一个测试用例。当你需要单独运行某个测试,或者要将其添加到某个测试包中时,你就可以使用TestCaller。一个TestCaller仅对应一个Test类,该Test类和一个TestFixture相关联。下面是一个演示的例子:
// 一个TestFixture,并包含了test method(s) class MathTest : public CppUnit::TestFixture { ... public: void setUp(); void tearDown(); void testAdd(); void testSubtract(); }; CppUnit::Test *MathTest::suite() { CppUnit::TestSuite *suite = new CppUnit::TestSuite; // 将MathTest::testAdd加入TestCaller,并将该TestCaller加入测试包中 suite->addTest( new CppUnit::TestCaller<MathTest>( "testAdd", testAdd ) ); return suite; }
你可是使用TestCaller,将任意一个test方法和某个TestFixture绑定在一起,只要该test方法满足如下形式的定义:
void testMethod(void);
TestCaller其实是一个模板类,它派生自TestCase,有两个模板类型参数,前一个参数代表了TestFixture类,后一个参数代表某个异常类,缺省类型为NoExceptionExpected,至于该参数的作用,稍后便知分晓。关于TestCase,请见core部分:
template <typename Fixture, typename ExpectedException = NoExceptionExpected> class TestCaller : public TestCase { //... };
TestCaller有三个private属性的成员变量:
Fixture *m_fixture; // 指向TestFixture实例的指针 bool m_ownFixture; // 若为true,则由TestCaller负责维护m_fixture的生命周期 TestMethod m_test; // 指向某个test方法的函数指针
TestMethod的定义如下:
typedef void (Fixture::*TestMethod)();
正是因为有如上定义,才限制了TestCaller只能支持形参和返回值均为为void类型的test方法。
来看一下TestCaller的ctor和dtor,你会发现,对于Fixture的安置工作,CppUnit的实现者可谓细心周到:
// 由TestCaller创建fixture,负责维护其生命周期 TestCaller( std::string name, TestMethod test ) : TestCase( name ), m_ownFixture( true ), m_fixture( new Fixture() ), m_test( test ) { } // 由外界传入fixture,TestCaller不负责维护其生命周期 TestCaller( std::string name, TestMethod test, Fixture& fixture) : TestCase( name ), m_ownFixture( false ), m_fixture( &fixture ), m_test( test ) { } // 由外界传入fixture,由TestCaller负责维护其生命周期 TestCaller( std::string name, TestMethod test, Fixture* fixture) : TestCase( name ), m_ownFixture( true ), m_fixture( fixture ), m_test( test ) { } // 根据m_ownFixture,决定是否销毁m_fixture ~TestCaller() { if (m_ownFixture) delete m_fixture; }
作为TestCaller的基类,TestCase派生自Test和TestFixture,因此TestCaller有义务实现如下三个虚函数。
void setUp() { // 简单地调用了m_fixture的setUp方法 m_fixture->setUp (); } void tearDown() { // 简单地调用了m_fixture的tearDown方法 m_fixture->tearDown (); } void runTest() { // 调用了m_fixture的一个test方法 // 由此可见: // - test方法必须在Fixture类中定义 // - 若Fixture类中存在多个test方法,则需一一建立与之对应的TestCaller try { (m_fixture->*m_test)(); } catch ( ExpectedException & ) { return; } ExpectedExceptionTraits<ExpectedException>::expectedException(); }
这里不得不提到辅助类ExpectedExceptionTraits,还有前面出现过的NoExceptionExpected,它们和TestCaller一起被定以在同一个文件里。
template<typename ExceptionType> struct ExpectedExceptionTraits { static void expectedException() { #if CPPUNIT_USE_TYPEINFO_NAME std::string message( "Expected exception of type " ); message += TypeInfoHelper::getClassName( typeid( ExceptionType ) ); message += ", but got none"; #else std::string message( "Expected exception but got none" ); #endif throw Exception( message ); } };
ExpectedExceptionTraits只有一个static方法,其唯一的作用是抛出一个Exception类型的异常,并附带一个说明信息,指出某个预计产生的异常并未出现,该预计的异常由ExpectedExceptionTraits的模板类型参数来指定。关于Exception,请见core部分。结合前面出现过的TestCaller::runTest的行为,我们可以得出如下结论:
通常情况下,如果调用m_fixture的test方法时,没有抛出任何异常,或者抛出的不是ExpectedException类型的异常,则ExpectedExceptionTraits的expectedException方法会产生一个异常来指出这一错误。其中的ExpectedException,由TestCaller的第二个类型参数来指定。
再看一下NoExceptionExpected的定义,它被作为TestCaller的第二个类型参数的缺省类型:
class CPPUNIT_API NoExceptionExpected { private: // 防止此类被实例化 NoExceptionExpected(); };
什么事都不做!是的,NoExceptionExpected只是一个Marker class,它用来表明,TestCaller在任何时候都不会对运行test方法时所抛出的异常做预期性检查。当然,光有了NoExceptionExpected还不够,还需要定义一个ExpectedExceptionTraits的特化版本:
template<> struct ExpectedExceptionTraits<NoExceptionExpected> { static void expectedException() { } };
同样是什么事都没做,再次结合TestCaller::runTest的行为,我们就可以得出如下结论:
当TestCaller的第二个类型参数为NoExceptionExpected时,如果调用m_fixture的test方法时
- 没有抛出任何异常,则ExpectedExceptionTraits的expectedException方法(特化版)将被调用,并且不抛出任何异常;
- 有异常被抛出,则一定不是NoExceptionExpected(因为那个private ctor),原样继续向外抛出此异常。
相关文件:AutoRegisterSuite.h
AutoRegisterSuite是一个模板类,其作用是自动注册指定类型的测试包,不过你不需要直接使用该类,而代之以如下的两个宏:
CPPUNIT_TEST_SUITE_REGISTRATION() CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()
关于宏,在随后的HelperMacros部分将会有更为详细的说明。
AutoRegisterSuite的全部内容是两个不同版本的ctor:
AutoRegisterSuite() { // 利用TestSuiteFactory创建一个类工厂实例factory TestFactory *factory = new TestSuiteFactory<TestCaseType>(); // 调用getRegistry(),在NamedRegistries中注册一个TestFactoryRegistry实例, // 注册项的名称为“All Tests”,并调用该实例的registerFactory,将factory注册为 // 其下所属的一个类工厂实例 TestFactoryRegistry::getRegistry().registerFactory( factory ); } AutoRegisterSuite( const std::string &name ) { // 除了注册项的名称由外部指定外,其余同前 TestFactory *factory = new TestSuiteFactory<TestCaseType>(); TestFactoryRegistry::getRegistry( name ).registerFactory( factory ); }
这里的TestCaseType是AutoRegisterSuite的模板类型参数。前面曾经提到过的TestSuiteFactory,其makeTest方法会调用TestCaseType的suite方法以创建测试实例,至于makeTest的调用时机,则和TestFactoryRegistry的makeTest方法有关。
相关文件:Outputter.h
这是一系列测试结果输出类的抽象基类,只有寥寥几行代码,唯一的作用是定义了一个write操作和一个virtual dtor:
virtual ~Outputter() {} virtual void write() =0;
由于各种输出方式,其具体实现大相径庭,所以Outputter所能做的也止于此了。
相关文件:TestResultCollector.h,TestResultCollector.cpp
该类派生自TestSucessListener,同样也是TestListener和SynchronizedObject,因为前者派生自后二者。关于TestSucessListener请见listener部分,关于TestListener请见core部分。TestResultCollector的作用是搜集正在执行的测试用例的结果。依源码中的documentation comments所述,这是Collecting Parameter Pattern的一个应用[该Pattern在GoF中没有提及,morning有些孤陋寡闻]
TestSucessListene定义了三个成员变量,用来记录测试相关信息:
std::deque<Test *> m_tests; // 指针队列用以记录测试对象 std::deque<TestFailure *> m_failures; // 指针队列用以记录测试失败信息 int m_testErrors; // 用以记录测试错误个数
TestSucessListene还覆盖了基类TestListener的startTest,reset和addFailure方法:
void TestResultCollector::startTest( Test *test ) { ExclusiveZone zone (m_syncObject); m_tests.push_back( test ); // 将测试对象加入链表中 } void TestResultCollector::reset() { TestSucessListener::reset(); ExclusiveZone zone( m_syncObject ); m_testErrors = 0; m_tests.clear(); m_failures.clear(); } void TestResultCollector::addFailure( const TestFailure &failure ) { TestSucessListener::addFailure( failure ); ExclusiveZone zone( m_syncObject ); if ( failure.isError() ) // 若failure实为error,则m_testErrors加1 ++m_testErrors; m_failures.push_back( failure.clone() ); // 此处用了clone }
这里使用了ExclusiveZone,关于failure和error的差别,以及clone方法,请见core部分。由于使用了clone方法,所以在addFailure中创建的failure需要在dtor中回收:
TestResultCollector::~TestResultCollector() { TestFailures::iterator itFailure = m_failures.begin(); while ( itFailure != m_failures.end() ) delete *itFailure++; }
此外,就是几个getter方法了:
// 获取运行的测试个数 int TestResultCollector::runTests() const { ExclusiveZone zone( m_syncObject ); return m_tests.size(); } // 获取运行错误的测试个数 int TestResultCollector::testErrors() const { ExclusiveZone zone( m_syncObject ); return m_testErrors; } // 获取运行失败的测试个数 int TestResultCollector::testFailures() const { ExclusiveZone zone( m_syncObject ); return m_failures.size() - m_testErrors; } // 获取运行错误及失败的测试的总个数 int TestResultCollector::testFailuresTotal() const { ExclusiveZone zone( m_syncObject ); return m_failures.size(); } // 获取记录测试失败的链表 const TestResultCollector::TestFailures & TestResultCollector::failures() const { ExclusiveZone zone( m_syncObject ); return m_failures; } // 获取记录测试对象的链表 const TestResultCollector::Tests & TestResultCollector::tests() const { ExclusiveZone zone( m_syncObject ); return m_tests; }
这里还想提一下有关TestResult和TestResultCollector的区别。根据随CppUnit所附的ChangeLog中的“记载”,早先版本的CppUnit中只有TestResult,它所实现的功能和TestResultCollector完全一样,像failures、testFailuresTotal等方法,原来都是在TestResult中的。不过在随后的refactoring过程中,由于引入了Observer Pattern,TestResultCollector应运而生,TestResult成了Subject,其原有记录测试运行结果的责任被移交给了TestResultCollector,而后者则是一个地道的Listener。于是原来在TestResult中出现的那些方法在Extract Method的“协助”之下被转移到了TestResultCollector中。
记得2003年的春节假期,难得有时间可以静下来充充电,于是有了研读CppUnit源码的念头。一来是为了熟悉CppUnit的使用环境,而来也是希望通过研读源码汲取有益的东西,这一系列的文章便是整理自笔者当初的源码阅读笔记。
如何将诸多技术综合运用到一个实际的framework中来,笔者以为,CppUnit为我们提供了一个难易适中的参考范例。这应该是一个很好的例子,因为它不甚复杂,却汇聚了一个framework所必需的某些设计思想以及实现技巧。在这里,我们可以看到STL的实际使用(包括一些简单的traits技法),Design Pattern的灵活运用(比如:Composite,Factory,Decorator,Singleton,Observer等)。
当然,也应该指出,由于CppUnit还在不断改进中,其代码中未免还有“败笔”及不尽如人意之处。但是,瑕不掩瑜,并且从中我们也可以感受到一个成熟框架的演进过程。
由于有过一点framework的设计经验和体会,笔者在阅读CppUnit源码的过程中,时常能有共鸣,并且对于框架的设计者在某些细节的处理方法,也深以为然,偶尔也有“英雄所见略同”的感叹。希望可以通过笔者的讲解,使大家也能够同样有亲历之感。
CppUnit是xUnit系列中的c++实现版本,它是从JUnit移植过来的,第一个移植版本由Michael Feathers完成,相关信息可以在http://www.xprogramming.com/software.htm找到。它是操作系统相关的,随后,Jerome Lacoste将之移植到了Unix/Solaris,在上述连接中也能找到该版本的相关信息。CppUnit项目就是基于这些版本建立起来的。有关CppUnit的讨论可以在http://c2.com/cgi/wiki?CppUnit找到,在那里你还可以找到CppUnit先前的版本以及许多其它操作系统环境下的移植版本。这个库受GNU LGPL(Lesser General Public License)的保护。作者包括:Eric Sommerlade (sommerlade@gmx.net),Michael Feathers (mfeathers@objectmentor.com),Jerome Lacoste (lacostej@altern.org),J.E. Hoffmann ,Baptiste Lepilleur ,Bastiaan Bakker ,Steve Robbins
这里所选用的是CppUnit 1.8.0版,你可以从http://sourceforge.net/projects/cppunit/下载到最新版本。
作为一个完整的CppUnit framework,虽然源码所在的实际路径可能不尽相关,但从逻辑上讲它们被划为如下几个部分:
- core:CppUnit的核心部分
- output:掌管结果输出
- helper:一些辅助类
- extension:作为单元测试的延伸,对CppUnit core部分的扩展(比如:常规测试,重复测试)
- listener:监视测试进程和测试结果
- textui:一个运行单元测试的文本环境
- portability:提供针对不同平台的移植设置
上述所有的内容均被置于CppUnit名字空间之内。
- 本文主要内容依据CppUnit源码而来,部分内容还来自于源码自身所附的注释、ChangeLog等
- 本文只作源码解读,至于xUnit家族的相关背景及基本知识笔者不准备叙述,读者可以参看相关文章
- 对于文中所涉及的Design Pattern,Refactoring,STL等相关知识,请读者参看相关资料。
- 除了文章本身,文中所列源码,也夹带了morning的一些注释,用以进一步说明代码意图,注释中方括号内为morning的疑问
- 为了节省篇幅、简化内容、突出主题,文中未列出全部代码,而是有选择的给出部分代码
- 由于工作的缘故,撰写这一系列的文章是陆续进行的,因此文字斟酌、行文的前后一致性方面不甚考究,在此请诸位见谅。如有必要且时间允许,morning将会对此作一完整的整理。
核心部分(Core)——基本测试类
在CppUnit中,有一个贯穿始终的最基本的pattern,那便是Composite Pattern。在GoF中对该pattern有如下描述:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。在CppUnit的框架中,测试类分为两种,某些测试类代表单个测试,比如稍后讲到的TestCase(测试用例),另一些则由若干测试类共同构成,比如稍后讲到的TestSuite(测试包)。彼此相关的TestCase共同构成一个TestSuite,而TestSuite也可以嵌套包含。两者分别对应Composite Pattern中的Leaf和Composite。
相关文件:Test.h
这是所有测试类的抽象基类,规定了所有测试类都应该具有的行为,对应于Composite Pattern中的Component,除了标准的virtual dtor外,还定义了四个纯虚函数:
// 运行测试内容,并利用传入其内的TestResult搜集测试结果,类似Component的Operation操作 virtual void run (TestResult *result) = 0; // 返回当前包含的测试对象的个数,若为TestCase,则返回1。 virtual int countTestCases () const = 0; // 返回测试的名称,每个测试都有一个名称,就像是标识,用以查找或显示 virtual std::string getName () const = 0; // 本测试的简短描述,用于调试输出。 // 对测试的描述除了名称外,可能还有其他信息, // 比如:一个名为“complex_add”的测试包可能被描述成“suite complex_add” virtual std::string toString () const = 0;
相关文件:TestFixture.h
该类也是抽象类,用于包装测试类使之具有setUp方法和tearDown方法。利用它,可以为一组相关的测试提供运行所需的公用环境(即所谓的fixture)。要实现这一目的,你需要:
- 从TestFixture派生一个子类(事实上,一般的做法是从TestCase派生,这样比较方便,具体见后)
- 定义实例变量(instance variables)以形成fixture
- 重载setUp初始化fixture的状态
- 重载tearDown在测试结束后作资源回收工作
此外,作为完整的测试类,还要定义一些执行具体测试任务的测试方法,然后使用TestCaller进行测试。关于TestCaller,在helper部分将会讲到。
因为每个测试对象运行在其自身的fixture中,所以测试对象之间不会有副作用(side effects),而测试对象内部的测试方法则共同使用同一个fixture。
来看一下TestFixture的定义,除了标准的virtual dtor外,还定义了两个纯虚函数:
// 在运行测试之前设置其上下文,即fixture // 一般而言setUp更为重要些,除非实例变量创建于heap中,否则其资源的回收就无需手工处理了 virtual void setUp() {}; // 在测试运行结束之后进行资源回收 virtual void tearDown() {};
相关文件:TestCase.h,TestCase.cpp
派生自Test和TestFixture(多重继承),兼具两者特性,用于实现一个简单的测试用例。你所要做的就是派生该类,并重载runTest方法。不过通常你不必如此,而是使用TestCaller结合TestFixture的方法,这样很方便。当你发现TestCaller无法满足,你需要重写一个功能近似的类时,再使用TestCase也不迟。关于TestCaller,在helper部分将会讲到。
TestCase中最重要的方法是run方法,来看一下代码,并请留意morning的注释:
void TestCase::run( TestResult *result ) { // 不必关心startTest的具体行为,在讲到TestResult时自然会明白 // 末尾的endTest亦是如此 result->startTest(this); try { // 设置fixture,具体内容需留待派生类解决 // 可能有异常抛出,处理方式见后 setUp(); // runTest具有protected属性,是真正执行测试的函数 // 但具体行为需留待派生类解决 try { runTest(); } // 在运行测试时可能会抛出异常,以下是异常处理 catch ( Exception &e ) { // Prototype Pattern的一个应用 // e是临时对象,addFailure调用之后即被销毁,所以需要创建一个副本 Exception *copy = e.clone(); result->addFailure( this, copy ); } catch ( std::exception &e ) { // 异常处理的常用方法——转意 result->addError( this, new Exception( e.what() ) ); } catch (...) { // 截获其余未知异常,一网打尽 Exception *e = new Exception( "caught unknown exception" ); result->addError( this, e ); } // 资源回收 try { tearDown(); } catch (...) { result->addError( this, new Exception( "tearDown() failed" ) ); } } catch (...) { result->addError( this, new Exception( "setUp() failed" ) ); } result->endTest( this ); }
可以看到,run方法定义了一个测试类运行的基本行为及其顺序:
- setUp:准备
- runTest:开始
- tearDown:结束
而TestCase作为抽象类无法确定测试的具体行为,因此需要留待派生类解决,这就是Template Method Pattern。事实上,该pattern在framework中是很常见的。因此一个完整测试的简单执行方法是,从TestCase派生一个类,重载相关方法,并直接调用run方法(正如TestFixture中所提到的)。
有意思的是,TestCase中还有run的另一个版本,它没有形参,而是创建一个缺省的TestResult,然后调用前述run方法。不过好像没怎么用到,大概是先前调试时未及清理的垃圾代码,也难怪会有“FIXME: what is this for?”这样的注释了。
TestCase有两个ctor:
TestCase( std::string Name ); // 测试类的名称 TestCase();
后者主要用于TestCaller,因为在使用TestCaller时,需要一个default ctor
此外,TestCase将copy ctor和operator=声明为private属性,以防止误用。
相关文件:TestSuite.h,TestSuite.cpp
一组相互关联的测试用例,构成了一个测试包,这就是TestSuite,也就是Composite Pattern中的Composite。和TestCase一样,也派生自Test,只是没有fixture特性。除了测试类的名称外,在TestSuite中还维护了一个测试对象数组,它被声明为private属性:
std::vector<Test *> m_tests; const std::string m_name;
来看一下TestSuite的run方法是如何实现的,并请留意morning的注释:
void TestSuite::run( TestResult *result ) { // 遍历vector<Test *> for ( std::vector<Test *>::iterator it = m_tests.begin(); it != m_tests.end(); ++it ) { // 可能中途终止 if ( result->shouldStop() ) break; Test *test = *it; // 调用每个test自己的run // 可能是TestCase实例,也可能是TestSuite实例, // 后者形成递归,但此处却全然不知 test->run( result ); } }
关于TestResult及其shouldStop方法,稍后会讲到。不过此处的break,到也算是活用Composite Pattern的一个简单范例。从效率的角度考虑,当确信不必再执行后续的test时,即可直接返回,而不是照葫芦画瓢,简单的调用一下test的run方法。
既然TestResult派生自Test,那么countTestCases又是如何实现的呢:
int TestSuite::countTestCases() const { int count = 0; // 遍历vector<Test *> for ( std::vector<Test *>::const_iterator it = m_tests.begin(); it != m_tests.end(); ++it ) count += (*it)->countTestCases(); // 递归调用每个test的countTestCases,并累加 return count; }
至于addTest,自然是不能少的,它对应于Composite的Add方法:
void TestSuite::addTest( Test *test ) { m_tests.push_back( test ); // 将test添加到测试对象数组的尾端 }
不过请注意,addTest方法并未出现于抽象类Test中,关于这类设计上的权衡在GoF中,Composite Pattern一节有专门的论述。
TestSuite管理着其下所属诸测试对象的生命周期,在dtor中,它会调用deleteContents方法:
void TestSuite::deleteContents() { for ( std::vector<Test *>::iterator it = m_tests.begin(); it != m_tests.end(); ++it) delete *it; m_tests.clear(); }
此外,TestSuite还为外部访问其所属测试对象提供了接口,因为返回值是const &类型的,所以是read-only的:
const std::vector<Test *> &getTests() const;