1. 程式人生 > >開放原始碼 C/C++ 單元測試工具,第 2 部分: 瞭解 CppUnit

開放原始碼 C/C++ 單元測試工具,第 2 部分: 瞭解 CppUnit

https://www.ibm.com/developerworks/cn/aix/library/au-ctools2_cppunit/index.html

本文是討論開放原始碼單元測試工具的 系列文章 的第 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 安裝目錄結構
[[email protected]] echo $CPPUNIT_HOME
/home/arpan/ibm/cppUnit
[[email protected]] 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 中程式碼的輸出
[[email protected]] ./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 中程式碼的輸出
[[email protected]] ./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 中程式碼的輸出
[[email protected]] ./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 中程式碼的輸出
[[email protected]] ./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 中程式碼的輸出
	[[email protected]] ./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 中程式碼的輸出
[[email protected]] ./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 文件來了解它的類層次結構,通過示例瞭解詳細的安裝資訊。