關於CppUnit裏面宏的介紹

本文是討論開放源碼單元測試工具的 系列文章 的第 2 篇,介紹非常受歡迎的 CppUnit — 最初由 Eric Gamma 和 Kent Beck 開發的 JUnit 測試框架的 C++ 版本。C++ 版本由 Michael Feathers 創建,它包含許多類,有助於進行白盒測試和創建自己的迴歸測試套件。本文介紹一些比較有用的 CppUnit 特性,比如 TestCase、TestSuite、TestFixture、TestRunner 和輔助宏。

常用縮寫詞

  • GUI:圖形用戶界面
  • XML:可擴展標記語言

下載和安裝 CppUnit

對於本文,我在一臺 Linux® 機器(內核 2.4.21)上用 g++-3.2.3 和 make-3.79.1 下載並安裝了 CppUnit。安裝過程很簡單,是標準的:運行 configure 命令,然後運行 makemake install。注意,對於 cygwin 等平臺,這個過程可能無法順利地完成,所以一定要通過 INSTALL-unix 文檔瞭解詳細的安裝信息。如果安裝成功,應該會在安裝路徑(CPPUNIT_HOME)中看到 CppUnit 的 include 和 lib 文件夾。清單 1 給出文件夾結構。


清單 1. CppUnit 安裝目錄結構
				
[arpan@tintin] echo $CPPUNIT_HOME
/home/arpan/ibm/cppUnit
[arpan@tintin] ls $CPPUNIT_HOME
bin  include  lib  man  share

要想編譯使用 CppUnit 的測試,必須構建源代碼:

g++ <C/C++ file> -I$CPPUNIT_HOME/include –L$CPPUNIT_HOME/lib -lcppunit

注意,如果是使用 CppUnit 的共享庫版本,可能需要使用 –ldl 選項編譯源代碼。安裝之後,還可能需要修改 UNIX® 環境變量 LD_LIBRARY_PATH 以反映 libcppunit.so 的位置。

使用 CppUnit 創建基本測試

學習 CppUnit 的最佳方法是創建一個葉級測試(leaf level test)。CppUnit 附帶一整套預先定義的類,可以用它們方便地設計測試。爲了保持連續性,先回顧一下本系列 第 1 部分 中討論過的字符串類(見 清單 2)。


清單 2. 簡單的字符串類
				
#ifndef _MYSTRING
#define _MYSTRING

class mystring { 
  char* buffer; 
  int length;
  public: 
    void setbuffer(char* s) { buffer = s; length = strlen(s); } 
    char& operator[ ] (const int index) { return buffer[index]; }
    int size( ) { return length; }
 }; 

#endif

與字符串相關的典型檢查包括檢查空字符串的長度是否爲 0 以及訪問範圍超出索引是否導致錯誤消息/異常。清單 3 使用 CppUnit 執行這些測試。


清單 3. 字符串類的單元測試
				
#include <cppunit/TestCase.h>
#include <cppunit/ui/text/TextTestRunner.h>

class mystringTest : public CppUnit::TestCase {
public:

  void runTest() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() != 0);
  }
};

int main ()
{
  mystringTest test;
  CppUnit::TextTestRunner runner;
  runner.addTest(&test);

  runner.run();
  return 0;
}

要學習的第一個 CppUnit 類是 TestCase。要想爲字符串類創建單元測試,需要創建 CppUnit::TestCase 類的子類並覆蓋 runTest 方法。定義了測試本身之後,實例化 TextTestRunner 類,這是一個控制器類,必須在其中添加測試(vide addTest 方法)。清單 4 給出 run 方法的輸出。


清單 4. 清單 3 中代碼的輸出
				
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run:  1   Failures: 1   Errors: 0


1) test:  (F) line: 26 try.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

爲了確認斷言確實起作用了,把 CPPUNIT_ASSERT_MESSAGE 宏中的條件改爲相反的條件。清單 5 給出條件改爲 s.size() ==0 之後代碼的輸出。


清單 5. 條件改爲 s.size( ) == 0 之後清單 3 中代碼的輸出
				
[arpan@tintin] ./a.out

OK (1 tests)

注意,TestRunner 並非運行單一測試或測試套件的惟一方法。CppUnit 還提供另一個類層次結構 — 即模板化的 TestCaller 類。可以不使用 runTest 方法,而是使用 TestCaller 類執行任何方法。清單 6 提供一個小示例。


清單 6. 使用 TestCaller 運行測試
				
class ComplexNumberTest ... { 
  public: 
     void ComplexNumberTest::testEquality( ) { … } 
};

CppUnit::TestCaller<ComplexNumberTest> test( "testEquality", 
                                             &ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );

在上面的示例中,定義了一個類型爲 ComplexNumberText 的類,其中包含 testEquality 方法(測試兩個複數是否相等)。用這個類對 TestCaller 進行模板化,與使用 TestRunner 時一樣,通過調用 run 方法執行測試。但是,這樣使用 TestCaller 類意義不大:TextTestRunner 類會自動顯示輸出。而在使用 TestCaller 時,必須使用另一個類處理輸出。在本文後面使用 TestCaller 類定義定製的測試套件時,您會看到這種代碼。

使用斷言


清單 7. CPPUNIT_ASSERT_MESSAGE 的定義
				
#define CPPUNIT_ASSERT_MESSAGE(message,condition)                          \
  ( CPPUNIT_NS::Asserter::failIf( !(condition),                            \
                                  CPPUNIT_NS::Message( "assertion failed", \
                                                       "Expression: "      \
                                                       #condition,         \
                                                       message ),          \
                                  CPPUNIT_SOURCELINE() ) )

清單 8 給出這個斷言使用的 failIf 方法的聲明。


清單 8. failIf 方法的聲明
				
struct Asserter
{
…
  static void CPPUNIT_API failIf( bool shouldFail,
                                  const Message &message,
                                  const SourceLine &sourceLine = SourceLine() );
…
}

如果 failIf 方法中的條件爲真,就會拋出一個異常。run 方法在內部處理該過程。另一個有意思、有用的宏是 CPPUNIT_ASSERT_DOUBLES_EQUAL,它使用一個容差值檢查兩個雙精度數是否相等(即 |expected – actual | ≤ delta)。清單 9 給出宏定義。


清單 9. CPPUNIT_ASSERT_DOUBLES_EQUAL 宏定義
				
void CPPUNIT_API assertDoubleEquals( double expected,
                                     double actual,
                                     double delta,
                                     SourceLine sourceLine,
                                     const std::string &message );
#define CPPUNIT_ASSERT_DOUBLES_EQUAL(expected,actual,delta)        \
  ( CPPUNIT_NS::assertDoubleEquals( (expected),            \
                                    (actual),              \
                                    (delta),               \
                                    CPPUNIT_SOURCELINE(),  \
                                    "" ) )

再次測試字符串類

爲了測試 mystring 類的其他方面,可以在 runTest 方法中添加更多檢查。但是,這麼做很快就會變得難以管理了,除非是最簡單的類。這時就需要定義和使用測試套件。清單 10 爲字符串類定義一個測試套件。


清單 10. 爲字符串類定義測試套件
				
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>

class mystringTest : public CppUnit::TestCase {
public:
  void checkLength() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
  }

  void checkValue() {
    mystring s;
    s.setbuffer("hello world!\n");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
  }

  CPPUNIT_TEST_SUITE( mystringTest );
  CPPUNIT_TEST( checkLength );
  CPPUNIT_TEST( checkValue );
  CPPUNIT_TEST_SUITE_END();
};

這很簡單。使用 CPPUNIT_TEST_SUITE 宏定義測試套件。mystringTest 類中的方法形成測試套件中的單元測試。我們稍後研究這些宏及其內容,但是先看看使用這個測試套件的客戶機代碼(見 清單 11)。


清單 11. 使用 mystring 類的測試套件的客戶機代碼
				
CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTest );

int main ()
{
  CppUnit::Test *test =
    CppUnit::TestFactoryRegistry::getRegistry().makeTest();
  CppUnit::TextTestRunner runner;
  runner.addTest(test);

  runner.run();
  return 0;
}

清單 12 給出運行 清單 11 時的輸出。


清單 12. 清單 10 和清單 11 中代碼的輸出
				
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run:  2   Failures: 2   Errors: 0


1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero


2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual  : w
- Corrupt String Data

CPPUNIT_ASSERT_EQUAL_MESSAGE 的定義在頭文件 TestAssert.h 中,它檢查預期參數和實際參數是否匹配。如果不匹配,就顯示指定的消息。在 HelperMacros.h 中定義的 CPPUNIT_TEST_SUITE 宏可以簡化創建測試套件並在其中添加測試的流程。在內部創建一個 CppUnit::TestSuiteBuilderContext 類型的模板化對象(這是 CppUnit 上下文中的測試套件),每個 CPPUNIT_TEST 調用在套件中添加相應的類方法。類方法作爲代碼的單元測試。請注意宏的次序:編譯各個 CPPUNIT_TEST 宏的代碼必須在 CPPUNIT_TEST_SUITECPPUNIT_TEST_SUITE_END 宏之間。

組織新測試

隨着時間的推移,開發人員會不斷添加功能,這些功能也需要測試。在同一測試套件中不斷添加測試會逐漸造成混亂,而且對首次測試的修改容易隨着修改的不斷增加而丟失。好在 CppUnit 提供一個有用的 CPPUNIT_TEST_SUB_SUITE 宏,可以使用它擴展現有的測試套件。清單 13 使用這個宏。


清單 13. 擴展測試套件
				
class mystringTestNew : public mystringTest {
public:
  CPPUNIT_TEST_SUB_SUITE (mystringTestNew, mystringTest);
  CPPUNIT_TEST( someMoreChecks );
  CPPUNIT_TEST_SUITE_END();

  void someMoreChecks() {
    std::cout << "Some more checks...\n";
  }
};

CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTestNew );

注意,新的類 mystringTestNew 是從前面的 myStringTest 類派生的。CPPUNIT_TEST_SUB_SUITE 宏的兩個參數是新的類和它的超類。在客戶端,只註冊這個新類,不需要註冊兩個類。語法的其他部分與創建測試套件的語法相同。

使用 fixtures 定製測試

在 CppUnit 上下文中,fixtureTestFixture 用於爲各個測試提供簡潔的設置和退出例程。要想使用 fixture,測試類應該派生自 CppUnit::TestFixture 並覆蓋預先定義的 setUptearDown 方法。在執行單元測試之前調用 setUp 方法,在測試執行完時調用 tearDown清單 14 演示如何使用 TestFixture


清單 14. 使用測試 fixture 定製測試套件
				
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>

class mystringTest : public CppUnit::TestFixture {
public:
  void setUp() { 
     std::cout << “Do some initialization here…\n”;
  }

  void tearDown() { 
      std::cout << “Cleanup actions post test execution…\n”;
  }

  void checkLength() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
  }

  void checkValue() {
    mystring s;
    s.setbuffer("hello world!\n");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
  }

  CPPUNIT_TEST_SUITE( mystringTest );
  CPPUNIT_TEST( checkLength );
  CPPUNIT_TEST( checkValue );
  CPPUNIT_TEST_SUITE_END();
};

清單 15 給出 清單 14 中代碼的輸出。


清單 15. 清單 14 中代碼的輸出
				
[arpan@tintin] ./a.out
. Do some initialization here…
FCleanup actions post test execution…
. Do some initialization here…
FCleanup actions post test execution…

!!!FAILURES!!!
Test Results:
Run:  2   Failures: 2   Errors: 0


1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero


2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual  : w
- Corrupt String Data

正如在輸出中看到的,每次執行單元測試都會顯示設置和清除例程消息。

創建不使用宏的測試套件

可以創建不使用任何輔助宏的測試套件。這兩種風格並沒有明顯的優劣,但是無宏風格的代碼更容易調試。要想創建不使用宏的測試套件,應該實例化 CppUnit::TestSuite,然後在套件中添加測試。最後,把套件本身傳遞給 CppUnit::TextTestRunner,然後再調用 run 方法。客戶端代碼很相似,見 清單 16


清單 16. 創建不使用輔助宏的測試套件
				
int main ()
{
  CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
                &mystringTest::checkLength));
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
                &mystringTest::checkLength));

  // client code follows next 
  CppUnit::TextTestRunner runner;
  runner.addTest(suite);

  runner.run();
  return 0;
}

要想理解 清單 16,需要理解 CppUnit 名稱空間中的兩個類:TestSuiteTestCaller(分別在 TestSuite.h 和 TestCaller.h 中聲明)。在執行 runner.run() 調用時,對於每個 TestCaller 對象,在 CppUnit 內部調用 runTest 方法,它進而調用傳遞給 TestCaller<mystringTest> 構造函數的例程。清單 17 中的代碼(取自 CppUnit 源代碼)說明如何爲每個套件調用測試。


清單 17. 執行套件中的測試
				
void
TestComposite::doRunChildTests( TestResult *controller )
{
  int childCount = getChildTestCount();
  for ( int index =0; index < childCount; ++index )
  {
    if ( controller->shouldStop() )
      break;

    getChildTestAt( index )->run( controller );
  }
}

TestSuite 類派生自 CppUnit::TestComposite

理解 CppUnit 中的指針

一定要在堆上聲明測試套件,因爲 CppUnit 在內部在 TestRunner 銷燬函數中刪除 TestSuite 指針。但是,這可能不是最好的設計決策,而且在 CppUnit 文檔中未被提及。

運行多個測試套件

可以創建多個測試套件並使用 TextTestRunner 在一個操作中運行它們。只需像 清單 16 那樣創建每個測試套件,然後使用 addTest 方法把它們添加到 TextTestRunner 中,見 清單 18


清單 18. 使用 TextTestRunner 運行多個套件
				
CppUnit::TestSuite* suite1 = new CppUnit::TestSuite("mystringTest");
suite1->addTest(…);
…
CppUnit::TestSuite* suite2 = new CppUnit::TestSuite("mymathTest");
…
suite2->addTest(…);
CppUnit::TextTestRunner runner;
runner.addTest(suite1);
runner.addTest(suite2);
…

定製輸出的格式

到目前爲止,測試的輸出都是由 TextTestRunner 類默認生成的。但是,CppUnit 允許使用定製的輸出格式。用於實現這個功能的類之一是 CompilerOutputter(在頭文件 CompilerOutputter.h 中聲明)。這個類允許指定輸出中文件名-行號信息的格式。另外,可以把日誌直接保存到文件中,而不是發送到屏幕。清單 19 提供一個把輸出轉儲到文件的示例。注意格式 %p:%l:前者表示文件的路徑,後者表示行號。使用這種格式時的典型輸出像 /home/arpan/work/str.cc:26 這樣。


清單 19. 把測試輸出轉發到日誌文件並採用定製的格式
				
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/CompilerOutputter.h>

int main ()
{
  CppUnit::Test *test =
    CppUnit::TestFactoryRegistry::getRegistry().makeTest();
  CppUnit::TextTestRunner runner;
  runner.addTest(test);

  const std::string format("%p:%l");
  std::ofstream ofile;
  ofile.open("run.log");
  CppUnit::CompilerOutputter* outputter = new
    CppUnit::CompilerOutputter(&runner.result(), ofile);
  outputter->setLocationFormat(format);
  runner.setOutputter(outputter);

  runner.run();
  ofile.close();
  return 0;
}

CompilerOutputter 還有很多其他有用的方法,比如可以使用 printStatisticsprintFailureReport 獲取它轉儲的信息的子集。

更多定製:跟蹤測試時間

到目前爲止,都是默認使用 TextTestRunner 運行測試。這種方式非常簡便:實例化一個 TextTestRunner 類型的對象,在其中添加測試和輸出器,然後調用 run 方法。現在,我們使用 TestRunnerTextTestRunner 的超類)和一種稱爲監聽器 的類改變這種運行過程。假設希望跟蹤各個測試花費的時間 — 執行性能基準測試的開發人員常常需要這樣做。在進一步解釋之前,先看一下 清單 20。這段代碼使用三個類 TestRunnerTestResultmyListener(派生自 TestListener)。這裏仍然使用 清單 10 中的 mystringTest 類。


清單 20. TestListener 類的使用
				
class myListener : public CppUnit::TestListener {
public:
  void startTest(CppUnit::Test* test) {
    std::cout << "starting to measure time\n";
  }
  void endTest(CppUnit::Test* test) {
    std::cout << "done with measuring time\n";
  }
};

int main ()
{
  CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
                &mystringTest::checkLength));
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
                &mystringTest::checkLength));

  CppUnit::TestRunner runner;
  runner.addTest(suite);

  myListener listener;
  CppUnit::TestResult result;
  result.addListener(&listener);

  runner.run(result);
  return 0;
}

清單 21 給出 清單 20 的輸出。


清單 21. 清單 20 中代碼的輸出
				
	[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time

myListener 類是 CppUnit::TestListener 的子類。需要覆蓋 startTestendTest 方法,這兩個方法分別在每個測試之前和之後執行。可以通過擴展這些方法輕鬆地檢查各個測試花費的時間。那麼,爲什麼不在設置/清除例程中添加這種功能呢?可以這麼做,但是這意味着在每個測試套件的設置/清除方法中會出現重複的代碼。

接下來,看看運行器對象,它是 TestRunner 類的實例,它在 run 方法中接收一個 TestResult 類型的參數,並在 TestResult 對象中添加監聽器。

最後,輸出結果會發生什麼變化?TextTestRunner 在運行 run 方法之後顯示許多信息,但是 TestRunner 不顯示這些信息。我們需要使用輸出器對象顯示監聽器對象在執行測試期間收集的信息。清單 22 顯示需要對 清單 20 做的修改。


清單 22. 添加輸出器以顯示測試執行信息
				
runner.run(result);
CppUnit::CompilerOutputter outputter( &listener, std::cerr );
outputter.write();

但是等一下:代碼還無法編譯。CompilerOutputter 的構造函數需要一個 TestResultCollector 類型的對象,而且因爲 TestResultCollector 本身派生自 TestListener(關於 CppUnit 類層次結構的詳細信息見 參考資料),所以需要從 TestResultCollector 派生 myListener清單 23 給出可編譯的代碼。


清單 23. 從 TestResultCollector 派生監聽器類
				
class myListener : public CppUnit::TestResultCollector {
…
};

int main ()
{
  …

  myListener listener;
  CppUnit::TestResult result;
  result.addListener(&listener);

  runner.run(result);

  CppUnit::CompilerOutputter outputter( &listener, std::cerr );
  outputter.write();

  return 0;
}

輸出見 清單 24


清單 24. 清單 23 中代碼的輸出
				
[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time
str.cc:31:Assertion
Test name: checkLength
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

str.cc:31:Assertion
Test name: checkValue
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

Failures !!!
Run: 0   Failure total: 2   Failures: 2   Errors: 0

結束語

本文主要討論了 CppUnit 框架的一些類:TestResultTestListenerTestRunnerCompilerOutputter 等。CppUnit 是一個獨立的單元測試框架,它還提供許多其他功能。CppUnit 中有用於生成 XML 輸出的類(XMLOutputter)和用於以 GUI 模式運行測試的類(MFCTestRunnerQtTestRunner),還提供一個插件接口(CppUnitTestPlugIn)。一定要查閱 CppUnit 文檔來了解它的類層次結構,通過示例瞭解詳細的安裝信息。


參考資料

學習

獲得產品和技術

  • 下載 CppUnit:獲取 CppUnit 的最新版本。

  • IBM 產品評估版:試用來自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的應用程序開發工具和中間件產品。

討論

轉載自IBM技術文章 http://www.ibm.com/developerworks/cn/aix/library/au-ctools2_cppunit/



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章