名稱 | 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 ([email protected]),Michael Feathers ([email protected]),Jerome Lacoste ([email protected]),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;