本文是討論開放源碼單元測試工具的
系列文章 的第 2 篇,介紹非常受歡迎的 CppUnit — 最初由 Eric Gamma 和 Kent Beck 開發的 JUnit 測試框架的
C++
版本。C++
版本由 Michael Feathers 創建,它包含許多類,有助於進行白盒測試和創建自己的迴歸測試套件。本文介紹一些比較有用的 CppUnit 特性,比如 TestCase、TestSuite、TestFixture、TestRunner 和輔助宏。
對於本文,我在一臺 Linux® 機器(內核 2.4.21)上用 g++-3.2.3 和 make-3.79.1 下載並安裝了 CppUnit。安裝過程很簡單,是標準的:運行
configure
命令,然後運行 make
和 make 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 的最佳方法是創建一個葉級測試(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. 清單 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_SUITE
和 CPPUNIT_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
宏的兩個參數是新的類和它的超類。在客戶端,只註冊這個新類,不需要註冊兩個類。語法的其他部分與創建測試套件的語法相同。
在 CppUnit 上下文中,fixture 或 TestFixture
用於爲各個測試提供簡潔的設置和退出例程。要想使用 fixture,測試類應該派生自
CppUnit::TestFixture
並覆蓋預先定義的 setUp
和 tearDown
方法。在執行單元測試之前調用
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 中代碼的輸出
[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 名稱空間中的兩個類:TestSuite
和 TestCaller
(分別在 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
。
可以創建多個測試套件並使用 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
還有很多其他有用的方法,比如可以使用 printStatistics
和
printFailureReport
獲取它轉儲的信息的子集。
到目前爲止,都是默認使用 TextTestRunner
運行測試。這種方式非常簡便:實例化一個 TextTestRunner
類型的對象,在其中添加測試和輸出器,然後調用
run
方法。現在,我們使用 TestRunner
(TextTestRunner
的超類)和一種稱爲監聽器 的類改變這種運行過程。假設希望跟蹤各個測試花費的時間 — 執行性能基準測試的開發人員常常需要這樣做。在進一步解釋之前,先看一下
清單 20。這段代碼使用三個類 TestRunner
、TestResult
和 myListener
(派生自
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 中代碼的輸出
[arpan@tintin] ./a.out starting to measure time done with measuring time starting to measure time done with measuring time |
myListener
類是 CppUnit::TestListener
的子類。需要覆蓋 startTest
和
endTest
方法,這兩個方法分別在每個測試之前和之後執行。可以通過擴展這些方法輕鬆地檢查各個測試花費的時間。那麼,爲什麼不在設置/清除例程中添加這種功能呢?可以這麼做,但是這意味着在每個測試套件的設置/清除方法中會出現重複的代碼。
接下來,看看運行器對象,它是 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 框架的一些類:TestResult
、TestListener
、TestRunner
、CompilerOutputter
等。CppUnit 是一個獨立的單元測試框架,它還提供許多其他功能。CppUnit 中有用於生成 XML 輸出的類(XMLOutputter
)和用於以 GUI 模式運行測試的類(MFCTestRunner
和 QtTestRunner
),還提供一個插件接口(CppUnitTestPlugIn
)。一定要查閱 CppUnit 文檔來了解它的類層次結構,通過示例瞭解詳細的安裝信息。
學習
- CppUnit 文檔:訪問 Sourceforge.com 上的項目頁面。
- CppUnit on Wikipedia:Wikipedia 包含關於 CppUnit 的信息以及其他單元測試框架的鏈接。
- developerWorks 技術活動和網絡廣播:隨時關注最新技術。
- 技術書店:在技術書店瀏覽關於這些主題和其他技術主題的圖書。
獲得產品和技術
- 下載 CppUnit:獲取 CppUnit 的最新版本。
- IBM 產品評估版:試用來自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的應用程序開發工具和中間件產品。
討論
- developerWorks 博客:閱讀我們的博客並加入
developerWorks 社區。
- 加入
My developerWorks 社區。
- 參與 AIX 和 UNIX 論壇:
轉載自IBM技術文章 http://www.ibm.com/developerworks/cn/aix/library/au-ctools2_cppunit/