復雜的 C/C++
代碼中很可能有 bug,到代碼編寫完成之后再來測試就像大海撈針。比較謹慎的辦法是,在編寫各個代碼段時,針對特定的區(qū)域(例如,一些包含大量計算的 C
函數(shù)或聲明隊列等數(shù)據(jù)結構的 C++
類),添加專門的小測試(單元測試),以在編寫代碼的同時進行測試。按這種方式構建的回歸測試套件包含一套單元測試和一個測試驅動程序,這個程序運行測試并報告結果。
對于文本編輯器這樣復雜的代碼,外部測試者無法生成針對特定例程的測試 — 測試者不太了解內部代碼組織。Boost 的優(yōu)勢就在于白箱測試 :由開發(fā)人員編寫測試,對類和函數(shù)進行語義檢查。這個過程極其重要,因為代碼以后的維護者可能會破壞原來的邏輯,這時單元測試就會失敗。通過使用白箱測試,常常很容易找到出錯的地方,不必使用調試器。
請考慮 清單 1 中的簡單字符串類。這個類并不健壯,我們使用 Boost 來測試它。
#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,訪問范圍超出索引是否導致錯誤消息或異常,等等。清單 2 給出了一些值得為任何字符串實現(xiàn)創(chuàng)建的測試。要想運行 清單 2 中的源代碼,只需用 g++(或任何符合標準的 C++
編譯器)編譯它。注意,不需要單獨的主函數(shù),代碼也不使用任何鏈接庫:作為 Boost 一部分的 unit_test.hpp 頭文件中包含所需的所有定義。
#define BOOST_TEST_MODULE stringtest #include <boost/test/included/unit_test.hpp> #include "./str.h" BOOST_AUTO_TEST_SUITE (stringtest) // name of the test suite is stringtest BOOST_AUTO_TEST_CASE (test1) { mystring s; BOOST_CHECK(s.size() == 0); } BOOST_AUTO_TEST_CASE (test2) { mystring s; s.setbuffer("hello world"); BOOST_REQUIRE_EQUAL ('h', s[0]); // basic test } BOOST_AUTO_TEST_SUITE_END( ) |
BOOST_AUTO_TEST_SUITE
和 BOOST_AUTO_TEST_SUITE_END
宏分別表示測試套件的開頭和結尾。各個測試放在這兩個宏之間,從這一點來看,這些宏的語義很像 C++
名稱空間。每個單元測試用 BOOST_AUTO_TEST_CASE
宏來定義。清單 3 給出了 清單 2 中代碼的輸出。
[arpan@tintin] ./a.out Running 2 test cases... test.cpp(10): error in "test1": check s.size() == 0 failed *** 1 failure detected in test suite "stringtest" |
下面詳細討論如何創(chuàng)建前面清單中的單元測試?;舅枷胧鞘褂?Boost 提供的宏來測試各個類特性。BOOST_CHECK
和 BOOST_REQUIRE_EQUAL
是 Boost 提供的預定義宏(也稱為測試工具),用于驗證代碼輸出。
Boost 有一整套測試工具,基本上可以說它們是用于驗證表達式的宏。測試工具的三個主要類別是 BOOST_WARN
、BOOST_CHECK
和 BOOST_REQUIRE
。BOOST_CHECK
和 BOOST_REQUIRE
之間的差異在于:對于前者,即使斷言失敗,測試仍然繼續(xù)執(zhí)行;而對于后者,認為這是嚴重的錯誤,測試會停止。清單 4 使用一個簡單的 C++
片段展示了這些工具類別之間的差異。
#define BOOST_TEST_MODULE enumtest #include <boost/test/included/unit_test.hpp> BOOST_AUTO_TEST_SUITE (enum-test) BOOST_AUTO_TEST_CASE (test1) { typedef enum {red = 8, blue, green = 1, yellow, black } color; color c = green; BOOST_WARN(sizeof(green) > sizeof(char)); BOOST_CHECK(c == 2); BOOST_REQUIRE(yellow > red); BOOST_CHECK(black != 4); } BOOST_AUTO_TEST_SUITE_END( ) |
第一個 BOOST_CHECK
會失敗,第一個 BOOST_REQUIRE
也是如此。但是,當 BOOST_REQUIRE
失敗時,代碼退出,所以不會到達第二個 BOOST_CHECK
。清單 5 顯示了 清單 4 中代碼的輸出。
[arpan@tintin] ./a.out Running 1 test case... e2.cpp(11): error in "test1": check c == 2 failed e2.cpp(12): fatal error in "test1": critical check yellow > red failed *** 2 failures detected in test suite "enumtest" |
同樣,如果需要針對特定情況檢查某些函數(shù)或類方法,最容易的方法是創(chuàng)建一個新測試,并使用參數(shù)和期望值調用這個例程。清單 6 提供了一個示例。
BOOST_AUTO_TEST(functionTest1) { BOOST_REQUIRE(myfunc1(99, ‘A’, 6.2) == 12); myClass o1(“hello world!\n”); BOOST_REQUIRE(o1.memoryNeeded( ) < 16); } |
經常需要根據(jù) “黃金日志” 測試函數(shù)生成的輸出。BOOST_CHECK
也適合執(zhí)行這種測試,這還需要用到 Boost 庫的 output_test_stream
類。用黃金日志文件(以下示例中的 run.log)初始化 output_test_stream
。C/C++
函數(shù)的輸出被提供給這個 output_test_stream
對象,然后調用這個對象的 match_pattern
例程。清單 7 提供了詳細代碼。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <boost/test/output_test_stream.hpp> using boost::test_tools::output_test_stream; BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { output_test_stream output( "run.log", true ); output << predefined_user_func( ); BOOST_CHECK( output.match_pattern() ); } BOOST_AUTO_TEST_SUITE_END( ) |
回歸測試中最棘手的檢查之一是浮點比較。請看一下 清單 8,看起來沒什么問題 — 至少從表面看是這樣。
#define BOOST_TEST_MODULE floatingTest #include <boost/test/included/unit_test.hpp> #include <cmath> BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { float f1 = 567.0102; float result = sqrt(f1); // this could be my_sqrt; faster implementation // for some specific DSP like hardware BOOST_CHECK(f1 == result * result); } BOOST_AUTO_TEST_SUITE_END( ) |
運行這個測試時,盡管使用的是作為標準庫一部分提供的 sqrt
函數(shù),BOOST_CHECK
宏仍然會失敗。什么地方出錯了?浮點比較的問題在于精度 — f1
和 result*result
在小數(shù)點后面的幾位不一致。為了解決這個問題,Boost 測試實用程序提供了 BOOST_WARN_CLOSE_FRACTION
、BOOST_CHECK_CLOSE_FRACTION
和 BOOST_REQUIRE_CLOSE_FRACTION
宏。要想使用這三個宏,必須包含預定義的 Boost 頭文件 floating_point_comparison.hpp。這三個宏的語法是相同的,所以本文只討論 check 變體(見 清單 9)。
BOOST_CHECK_CLOSE_FRACTION (left-value, right-value, tolerance-limit); |
清單 9 中沒有使用 BOOST_CHECK
,而是使用 BOOST_CHECK_CLOSE_FRACTION
并指定公差限制為 0.0001。清單 10 給出了代碼現(xiàn)在的樣子。
#define BOOST_TEST_MODULE floatingTest #include <boost/test/included/unit_test.hpp> #include <boost/test/floating_point_comparison.hpp> #include <cmath> BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { float f1 = 567.01012; float result = sqrt(f1); // this could be my_sqrt; faster implementation // for some specific DSP like hardware BOOST_CHECK_CLOSE_FRACTION (f1, result * result, 0.0001); } BOOST_AUTO_TEST_SUITE_END( ) |
這段代碼運行正常?,F(xiàn)在,把 清單 10 中的公差限制改為 0.0000001。清單 11 給出了輸出。
[arpan@tintin] ./a.out Running 1 test case... sq.cpp(18): error in "test": difference between f1{567.010132} and result * result{567.010193} exceeds 1e-07 *** 1 failure detected in test suite "floatingTest" |
生產軟件中另一個常見的問題是比較 double
和 float
類型的變量。BOOST_CHECK_CLOSE_FRACTION
的優(yōu)點是它不允許進行這種比較。這個宏中的左值和右值必須是相同類型的 — 即要么是 float
,要么是 double
。在 清單 12 中,如果 f1
是 double,而 result
是 float,在比較時就會出現(xiàn)錯誤。
[arpan@tintin] g++ sq.cpp -I/u/c/lib/boost /u/c/lib/boost/boost/test/test_tools.hpp: In function `bool boost::test_tools::tt_detail::check_frwd(Pred, const boost::unit_test::lazy_ostream&, boost::test_tools::const_string, size_t, boost::test_tools::tt_detail::tool_level, boost::test_tools::tt_detail::check_type, const Arg0&, const char*, const Arg1&, const char*, const Arg2&, const char*) [with Pred = boost::test_tools::check_is_close_t, Arg0 = double, Arg1 = float, Arg2 = boost::test_tools::fraction_tolerance_t<double>]': sq.cpp:18: instantiated from here /u/c/lib/boost/boost/test/test_tools.hpp:523: error: no match for call to `(boost::test_tools::check_is_close_t) (const double&, const float&, const boost::test_tools::fraction_tolerance_t<double>&)' |
Boost 測試工具驗證 Boolean 條件??梢酝ㄟ^擴展測試工具支持更復雜的檢查 — 例如,判斷兩個列表的內容是否相同,或者某一條件對于向量的所有元素是否都是有效的。還可以通過擴展 BOOST_CHECK
宏執(zhí)行定制的斷言檢查。下面對用戶定義的 C
函數(shù)生成的列表內容執(zhí)行定制的檢查:檢查結果中的所有元素是否都大于 1。定制檢查函數(shù)需要返回 boost::test_tools::predicate_result
類型。清單 13 給出了詳細的代碼。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> boost::test_tools::predicate_result validate_list(std::list<int>& L1) { std::list<int>::iterator it1 = L1.begin( ); for (; it1 != L1.end( ); ++it1) { if (*it1 <= 1) return false; } return true; } BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { std::list<int>& list1 = user_defined_func( ); BOOST_CHECK( validate_list(list1) ); } BOOST_AUTO_TEST_SUITE_END( ) |
predicate_result
對象有一個隱式的構造函數(shù),它接受一個 Boolean 值,因此即使 validate_list
的期望類型和實際返回類型不同,代碼仍然會正常運行。
還有另一種用 Boost 測試復雜斷言的方法:BOOST_CHECK_PREDICATE
宏。這個宏的優(yōu)點是它不使用 predicate_result
。但缺點是語法有點兒粗糙。用戶需要向 BOOST_CHECK_PREDICATE
宏傳遞函數(shù)名和參數(shù)。清單 14 的功能與 清單 13 相同,但是使用的宏不同。注意,validate_result
的返回類型現(xiàn)在是 Boolean。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> bool validate_list(std::list<int>& L1) { std::list<int>::iterator it1 = L1.begin( ); for (; it1 != L1.end( ); ++it1) { if (*it1 <= 1) return false; } return true; } BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { std::list<int>& list1 = user_defined_func( ); BOOST_CHECK_PREDICATE( validate_list, list1 ); } BOOST_AUTO_TEST_SUITE_END( ) |
可以在一個文件中包含多個測試套件。文件中定義的每個測試套件必須有一對 BOOST_AUTO_TEST_SUITE... BOOST_AUTO_TEST_SUITE_END
宏。清單 15 給出了在同一個文件中定義的兩個測試套件。在運行回歸測試時,用預定義的 –log_level=test_suite
選項運行可執(zhí)行程序。在 清單 16 中可以看到,使用這個選項生成的輸出很詳細,有助于進行快速調試。
#define BOOST_TEST_MODULE Regression #include <boost/test/included/unit_test.hpp> typedef struct { int c; char d; double e; bool f; } Node; typedef union { int c; char d; double e; bool f; } Node2; BOOST_AUTO_TEST_SUITE(Structure) BOOST_AUTO_TEST_CASE(Test1) { Node n; BOOST_CHECK(sizeof(n) < 12); } BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(Union) BOOST_AUTO_TEST_CASE(Test1) { Node2 n; BOOST_CHECK(sizeof(n) == sizeof(double)); } BOOST_AUTO_TEST_SUITE_END() |
下面是 清單 15 中代碼的輸出:
[arpan@tintin] ./a.out --log_level=test_suite Running 2 test cases... Entering test suite "Regression" Entering test suite "Structure" Entering test case "Test1" m2.cpp(23): error in "Test1": check sizeof(n) < 12 failed Leaving test case "Test1" Leaving test suite "Structure" Entering test suite "Union" Entering test case "Test1" Leaving test case "Test1" Leaving test suite "Union" Leaving test suite "Regression" *** 1 failure detected in test suite "Regression" |
到目前為止,本文已經討論了 Boost 測試實用程序和沒有層次結構的測試套件?,F(xiàn)在,我們使用 Boost創(chuàng)建一個測試套件,以外部工具用戶常見的方式測試軟件產品。在測試框架中,通常有多個套件,每個套件檢查產品的某些特性。例如,文字處理程序的回歸測試框架應該包含檢查字體支持、不同的文件格式等方面的套件。每個測試套件包含多個單元測試。清單 17 提供了一個測試框架示例。注意,代碼入口點必須是名為 init_unit_test_suite
的例程。
#define BOOST_TEST_MODULE MasterTestSuite #include <boost/test/included/unit_test.hpp> using boost::unit_test; test_suite* init_unit_test_suite( int argc, char* argv[] ) { test_suite* ts1 = BOOST_TEST_SUITE( "test_suite1" ); ts1->add( BOOST_TEST_CASE( &test_case1 ) ); ts1->add( BOOST_TEST_CASE( &test_case2 ) ); test_suite* ts2 = BOOST_TEST_SUITE( "test_suite2" ); ts2->add( BOOST_TEST_CASE( &test_case3 ) ); ts2->add( BOOST_TEST_CASE( &test_case4 ) ); framework::master_test_suite().add( ts1 ); framework::master_test_suite().add( ts2 ); return 0; } |
每個測試套件(比如 清單 17 中的 ts1
)都是使用 BOOST_TEST_SUITE
宏創(chuàng)建的。這個宏需要一個字符串作為測試套件的名稱。最終使用 add
方法,把所有測試套件添加到主測試套件中。同樣,我們使用 BOOST_TEST_CASE
宏創(chuàng)建每個測試,然后再使用 add
方法把它們添加到測試套件中。也可以把單元測試添加到主測試套件中,但是不建議這么做。master_test_suite
方法屬于 boost::unit_test::framework
名稱空間的一部分:它在內部實現(xiàn)一個單實例對象。清單 18 中的代碼取自 Boost 源代碼本身,解釋了這個方法的工作方式。
master_test_suite_t& master_test_suite() { if( !s_frk_impl().m_master_test_suite ) s_frk_impl().m_master_test_suite = new master_test_suite_t; return *s_frk_impl().m_master_test_suite; } |
使用 BOOST_TEST_CASE
宏創(chuàng)建的單元測試以函數(shù)指針作為輸入參數(shù)。在 清單 17 中,test_case1
、test_case2
等是 void 函數(shù),用戶可以按自己喜歡的方式編寫代碼。但是注意,Boost 測試設置會使用一些堆內存;每個對 BOOST_TEST_SUITE
的調用都會產生一個新的 boost::unit_test::test_suite(<test suite name>)
。
從概念上講,測試裝備(test fixture)是指在執(zhí)行測試之前設置一個環(huán)境,在測試完成時清除它。清單 19 提供了一個簡單的示例。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() : i( 0 ) { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } int i; }; BOOST_AUTO_TEST_SUITE( test ) BOOST_FIXTURE_TEST_CASE( test_case1, F ) { BOOST_CHECK( i == 1 ); ++i; } BOOST_AUTO_TEST_SUITE_END() |
清單 20 給出了輸出。
[arpan@tintin] ./a.out Running 1 test case... setup fix.cpp(16): error in "test_case1": check i == 1 failed teardown *** 1 failure detected in test suite "example" |
這段代碼沒有使用 BOOST_AUTO_TEST_CASE
宏,而是使用 BOOST_FIXTURE_TEST_CASE
,它需要另一個輸入參數(shù)。這個對象的 constructor
和 destructor
方法執(zhí)行必需的設置和清除工作。看一下 Boost 頭文件 unit_test_suite.hpp 就可以確認這一點(見 清單 21)。
#define BOOST_FIXTURE_TEST_CASE( test_name, F ) struct test_name : public F { void test_method(); }; static void BOOST_AUTO_TC_INVOKER( test_name )() { test_name t; t.test_method(); } struct BOOST_AUTO_TC_UNIQUE_ID( test_name ) {}; BOOST_AUTO_TU_REGISTRAR( test_name )( boost::unit_test::make_test_case( &BOOST_AUTO_TC_INVOKER( test_name ), #test_name ), boost::unit_test::ut_detail::auto_tc_exp_fail< BOOST_AUTO_TC_UNIQUE_ID( test_name )>::instance()->value() ); void test_name::test_method() |
在內部,Boost 從 struct F
公共地派生一個類(見 清單 19),然后從這個類創(chuàng)建對象。按照 C++
的公共繼承規(guī)則,在函數(shù)中可以直接訪問 struct
類的所有受保護變量和公共變量。注意,在 清單 19 中修改的變量 i
屬于類型為 F
的內部對象 t
(見 清單 20)。在回歸測試套件中可能只有幾個測試需要某種顯式的初始化,因此可以只對它們使用裝備特性。在 清單 22 給出的測試套件中,三個測試中只有一個使用裝備。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() : i( 0 ) { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } int i; }; BOOST_AUTO_TEST_SUITE( test ) BOOST_FIXTURE_TEST_CASE( test_case1, F ) { BOOST_CHECK( i == 1 ); ++i; } BOOST_AUTO_TEST_CASE( test_case2 ) { BOOST_REQUIRE( 2 > 1 ); } BOOST_AUTO_TEST_CASE( test_case3 ) { int i = 1; BOOST_CHECK_EQUAL( i, 1 ); ++i; } BOOST_AUTO_TEST_SUITE_END() |
在 清單 22 中,在一個測試用例上定義和使用了裝備。Boost 還允許用戶通過 BOOST_GLOBAL_FIXTURE (<Fixture Name>)
宏定義和使用全局裝備??梢远x任意數(shù)量的全局裝備,因此可以把初始化代碼分割為多個部分。清單 23 使用一個全局裝備。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } }; BOOST_AUTO_TEST_SUITE( test ) BOOST_GLOBAL_FIXTURE( F ); BOOST_AUTO_TEST_CASE( test_case1 ) { BOOST_CHECK( true ); } BOOST_AUTO_TEST_SUITE_END() |
對于多個裝備,它們的設置和清除按照聲明的次序執(zhí)行。在 清單 24 中,先調用 F
的構造函數(shù),然后是 F2
的;對于銷毀函數(shù)也是這樣。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } }; struct F2 { F2() { std::cout << "setup 2" << std::endl; } ~F2() { std::cout << "teardown 2" << std::endl; } }; BOOST_AUTO_TEST_SUITE( test ) BOOST_GLOBAL_FIXTURE( F ); BOOST_GLOBAL_FIXTURE( F2 ); BOOST_AUTO_TEST_CASE( test_case1 ) { BOOST_CHECK( true ); } BOOST_AUTO_TEST_SUITE_END() |
注意,不能將全局裝備作為對象用在單個測試中。也不能在測試中直接訪問它們的公共/受保護的非靜態(tài)方法或變量。
本文介紹了最強大的開放源碼回歸測試框架之一:Boost。討論了基本的 Boost檢查、模式匹配、浮點比較、定制檢查、測試套件的組織(包括手工和自動)和裝備。請通過 Boost 文檔了解更多信息。本系列的后續(xù)文章將介紹cppUnit 等開放源碼回歸測試框架。