CppUnit源碼解讀

名稱 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管理着其下所有測試對象的生命週期。

[TestRunner]

輸出部分(Portability)


這一部分,通過若干參數的設定,解決了向不同平臺移植時遇到的問題。另外還有一個叫做OStringStream的輔助類,不過morning以爲,該類似乎置於helper部分更爲合適。

[OStringStream] [其他]

附錄(Appendix)——WIN32平臺安裝說明


目前,CPPUnit在WIN32平臺下僅支持Microsoft Visual C++,而且你的VC++編譯器至少應該是6.0版本的。

使用GUI TestRunner編譯運行示例程序的步驟如下:

  1. 在VC++中打開examples/examples.dsw(包含所有的示例)
  2. 將HostApp設爲active project
  3. 編譯之
  4. 在VC中選擇Tools/Customize.../Add-ins and Macro Files,點擊Browse...
  5. 選擇lib/TestRunnerDSPlugIn.dll文件,並按ok以註冊該附加件(add-ins)
  6. 運行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)]

  1. 在VC++中打開src/CppUnitLibraries.dsw工作區文件。
  2. 將TestPlugInRunner設爲active project。
  3. 在'Build'菜單中選擇'Batch Build...'
  4. 在Batch Build對話框中,選中所有的project 並按下build按鈕。
  5. 所有的庫文件可以在lib/目錄下找到。

[測試(Testing)]

  1. 打開工作區文件examples/Examples.dsw。
  2. 將CppUnitTestApp設爲active project.
  3. 爲你要創建的庫選擇合適的配置。
  4. 編譯運行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;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章