什么是單元測試?
復(fù)雜的 C/C++ 代碼中很可能有 bug,到代碼編寫完成之后再來測試就像大海撈針。比較謹慎的辦法是,在編寫各個代碼段時,針對特定的區(qū)域(例如,一些包含大量計算的 C 函數(shù)或聲明隊列等數(shù)據(jù)結(jié)構(gòu)的 C++ 類),添加專門的小測試(單元測試),以在編寫代碼的同時進行測試。按這種方式構(gòu)建的回歸測試套件包含一套單元測試和一個測試驅(qū)動程序,這個程序運行測試并報告結(jié)果。
回頁首
為特定的函數(shù)或類生成測試
對于文本編輯器這樣復(fù)雜的代碼,外部測試者無法生成針對特定例程的測試 — 測試者不太了解內(nèi)部代碼組織。Boost 的優(yōu)勢就在于白箱測試 :由開發(fā)人員編寫測試,對類和函數(shù)進行語義檢查。這個過程極其重要,因為代碼以后的維護者可能會破壞原來的邏輯,這時單元測試就會失敗。通過使用白箱測試,常常很容易找到出錯的地方,不必使用調(diào)試器。
請考慮 清單 1 中的簡單字符串類。這個類并不健壯,我們使用 Boost 來測試它。
清單 1. 簡單的字符串類
#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
|
與字符串相關(guān)的一些典型的檢查,會驗證空字符串的長度是否為 0,訪問范圍超出索引是否導(dǎo)致錯誤消息或異常,等等。清單 2 給出了一些值得為任何字符串實現(xiàn)創(chuàng)建的測試。要想運行 清單 2 中的源代碼,只需用 g++(或任何符合標(biāo)準(zhǔn)的 C++ 編譯器)編譯它。注意,不需要單獨的主函數(shù),代碼也不使用任何鏈接庫:作為 Boost 一部分的 unit_test.hpp 頭文件中包含所需的所有定義。
清單 2. 字符串類的單元測試
#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 宏分別表示測試套件的開頭和結(jié)尾。各個測試放在這兩個宏之間,從這一點來看,這些宏的語義很像 C++ 名稱空間。每個單元測試用 BOOST_AUTO_TEST_CASE 宏來定義。清單 3 給出了 清單 2 中代碼的輸出。
清單 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 提供的預(yù)定義宏(也稱為測試工具),用于驗證代碼輸出。
回頁首
Boost 測試工具
Boost 有一整套測試工具,基本上可以說它們是用于驗證表達式的宏。測試工具的三個主要類別是 BOOST_WARN、BOOST_CHECK 和 BOOST_REQUIRE。BOOST_CHECK 和 BOOST_REQUIRE 之間的差異在于:對于前者,即使斷言失敗,測試仍然繼續(xù)執(zhí)行;而對于后者,認為這是嚴(yán)重的錯誤,測試會停止。清單 4 使用一個簡單的 C++ 片段展示了這些工具類別之間的差異。
清單 4. 使用 Boost 測試工具的三個變體
#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 也是如此。但是,當(dāng) BOOST_REQUIRE 失敗時,代碼退出,所以不會到達第二個 BOOST_CHECK。清單 5 顯示了 清單 4 中代碼的輸出。
清單 5. 理解 BOOST_REQUIRE 和 BOOST_CHECK 之間的差異
[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ù)和期望值調(diào)用這個例程。清單 6 提供了一個示例。
清單 6. 使用 Boost 測試檢查函數(shù)和類方法
BOOST_AUTO_TEST(functionTest1)
{
BOOST_REQUIRE(myfunc1(99, ‘A’, 6.2) == 12);
myClass o1(“hello world!\n”);
BOOST_REQUIRE(o1.memoryNeeded( ) < 16);
}
|
模式匹配
經(jīng)常需要根據(jù) “黃金日志” 測試函數(shù)生成的輸出。BOOST_CHECK 也適合執(zhí)行這種測試,這還需要用到 Boost 庫的 output_test_stream 類。用黃金日志文件(以下示例中的 run.log)初始化 output_test_stream。C/C++ 函數(shù)的輸出被提供給這個 output_test_stream 對象,然后調(diào)用這個對象的 match_pattern 例程。清單 7 提供了詳細代碼。
清單 7. 根據(jù)黃金日志文件執(zhí)行模式匹配
#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,看起來沒什么問題 — 至少從表面看是這樣。
清單 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( )
|
運行這個測試時,盡管使用的是作為標(biāo)準(zhǔn)庫一部分提供的 sqrt 函數(shù),BOOST_CHECK 宏仍然會失敗。什么地方出錯了?浮點比較的問題在于精度 —
f1 和 result*result 在小數(shù)點后面的幾位不一致。為了解決這個問題,Boost 測試實用程序提供了 BOOST_WARN_CLOSE_FRACTION、BOOST_CHECK_CLOSE_FRACTION 和 BOOST_REQUIRE_CLOSE_FRACTION 宏。要想使用這三個宏,必須包含預(yù)定義的 Boost 頭文件 floating_point_comparison.hpp。這三個宏的語法是相同的,所以本文只討論 check 變體(見 清單 9)。
清單 9. BOOST_CHECK_CLOSE_FRACTION 宏的語法
BOOST_CHECK_CLOSE_FRACTION (left-value, right-value, tolerance-limit);
|
清單 9 中沒有使用 BOOST_CHECK,而是使用 BOOST_CHECK_CLOSE_FRACTION 并指定公差限制為 0.0001。清單 10 給出了代碼現(xiàn)在的樣子。
清單 10. 有效的浮點比較
#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 給出了輸出。
清單 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"
|
生產(chǎn)軟件中另一個常見的問題是比較 double 和 float 類型的變量。BOOST_CHECK_CLOSE_FRACTION 的優(yōu)點是它不允許進行這種比較。這個宏中的左值和右值必須是相同類型的 — 即要么是 float,要么是 double。在 清單 12 中,如果 f1 是 double,而 result 是 float,在比較時就會出現(xiàn)錯誤。
清單 12. 錯誤:BOOST_CHECK_CLOSE_FRACTION 的左值和右值參數(shù)的類型不同
[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 條件。可以通過擴展測試工具支持更復(fù)雜的檢查 — 例如,判斷兩個列表的內(nèi)容是否相同,或者某一條件對于向量的所有元素是否都是有效的。還可以通過擴展 BOOST_CHECK 宏執(zhí)行定制的斷言檢查。下面對用戶定義的 C 函數(shù)生成的列表內(nèi)容執(zhí)行定制的檢查:檢查結(jié)果中的所有元素是否都大于 1。定制檢查函數(shù)需要返回 boost::test_tools::predicate_result 類型。清單 13 給出了詳細的代碼。
清單 13. 使用 Boost 測試工具驗證復(fù)雜的斷言
#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 對象有一個隱式的構(gòu)造函數(shù),它接受一個 Boolean 值,因此即使 validate_list 的期望類型和實際返回類型不同,代碼仍然會正常運行。
還有另一種用 Boost 測試復(fù)雜斷言的方法:BOOST_CHECK_PREDICATE 宏。這個宏的優(yōu)點是它不使用 predicate_result。但缺點是語法有點兒粗糙。用戶需要向 BOOST_CHECK_PREDICATE 宏傳遞函數(shù)名和參數(shù)。清單 14 的功能與 清單 13 相同,但是使用的宏不同。注意,validate_result 的返回類型現(xiàn)在是 Boolean。
清單 14. BOOST_CHECK_PREDICATE 宏
#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 給出了在同一個文件中定義的兩個測試套件。在運行回歸測試時,用預(yù)定義的 –log_level=test_suite 選項運行可執(zhí)行程序。在 清單 16 中可以看到,使用這個選項生成的輸出很詳細,有助于進行快速調(diào)試。
清單 15. 使用一個文件中的多個測試套件
#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 中代碼的輸出:
清單 16. 用 –log_level 選項運行多個測試套件
[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"
|
回頁首
理解測試套件的組織
到目前為止,本文已經(jīng)討論了 Boost 測試實用程序和沒有層次結(jié)構(gòu)的測試套件。現(xiàn)在,我們使用 Boost
創(chuàng)建一個測試套件,以外部工具用戶常見的方式測試軟件產(chǎn)品。在測試框架中,通常有多個套件,每個套件檢查產(chǎn)品的某些特性。例如,文字處理程序的回歸測試框
架應(yīng)該包含檢查字體支持、不同的文件格式等方面的套件。每個測試套件包含多個單元測試。清單 17 提供了一個測試框架示例。注意,代碼入口點必須是名為 init_unit_test_suite 的例程。
清單 17. 創(chuàng)建用于運行回歸測試的主測試套件
#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 名稱空間的一部分:它在內(nèi)部實現(xiàn)一個單實例對象。清單 18 中的代碼取自 Boost 源代碼本身,解釋了這個方法的工作方式。
清單 18. 理解 master_test_suite 方法
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ù)指針作為輸入?yún)?shù)。在 清單 17 中,test_case1、test_case2 等是 void 函數(shù),用戶可以按自己喜歡的方式編寫代碼。但是注意,Boost 測試設(shè)置會使用一些堆內(nèi)存;每個對 BOOST_TEST_SUITE 的調(diào)用都會產(chǎn)生一個新的 boost::unit_test::test_suite(<test suite name>)。
回頁首
裝備
從概念上講,測試裝備(test fixture)是指在執(zhí)行測試之前設(shè)置一個環(huán)境,在測試完成時清除它。清單 19 提供了一個簡單的示例。
清單 19. 基本的 Boost 裝備
#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 給出了輸出。
清單 20. Boost 裝備的輸出
[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,它需要另一個輸入?yún)?shù)。這個對象的 constructor 和 destructor 方法執(zhí)行必需的設(shè)置和清除工作??匆幌?Boost 頭文件 unit_test_suite.hpp 就可以確認這一點(見 清單 21)。
清單 21. 頭文件 unit_test_suite.hpp 中的 Boost 裝備定義
#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()
|
在內(nèi)部,Boost 從 struct F 公共地派生一個類(見 清單 19),然后從這個類創(chuàng)建對象。按照 C++ 的公共繼承規(guī)則,在函數(shù)中可以直接訪問 struct 類的所有受保護變量和公共變量。注意,在 清單 19 中修改的變量 i 屬于類型為 F 的內(nèi)部對象 t(見 清單 20)。在回歸測試套件中可能只有幾個測試需要某種顯式的初始化,因此可以只對它們使用裝備特性。在 清單 22 給出的測試套件中,三個測試中只有一個使用裝備。
清單 22. 同時包含裝備和非裝備測試的 Boost 測試套件
#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 使用一個全局裝備。
清單 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()
|
對于多個裝備,它們的設(shè)置和清除按照聲明的次序執(zhí)行。在 清單 24 中,先調(diào)用 F 的構(gòu)造函數(shù),然后是 F2 的;對于銷毀函數(shù)也是這樣。
清單 24. 在回歸測試中使用多個全局裝備
#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)方法或變量。
回頁首
結(jié)束語
本文介紹了最強大的開放源碼回歸測試框架之一:Boost。討論了基本的 Boost
檢查、模式匹配、浮點比較、定制檢查、測試套件的組織(包括手工和自動)和裝備。請通過 Boost 文檔了解更多信息。本系列的后續(xù)文章將介紹
cppUnit 等開放源碼回歸測試框架。