1. 程式人生 > >深入理解gtest:C/C++單元測試經驗談

深入理解gtest:C/C++單元測試經驗談

GoogleC++TestingFramework(簡稱gtest,http://code。google。com/p/googletest/)是Google公司釋出的一個開源C/C++單元測試框架,已被應用於多個開源專案及Google內部專案中,知名的例子包括ChromeWeb瀏覽器、LLVM編譯器架構、ProtocolBuffers資料交換格式及工具等。

優秀的C/C++單元測試框架並不算少,相比之下gtest仍具有明顯優勢。與CppUnit比,gtest需要使用的標頭檔案和函式巨集更集中,並支援測試用例的自動註冊。與CxxUnit比,gtest不要求Python等外部工具的存在。與Boost。Test比,gtest更簡潔容易上手,實用性也並不遜色。Wikipedia給出了各種程式語言的單元測試框架列表(

http://en。wikipedia。org/wiki/List_of_unit_testing_frameworks)。

一、基本用法

gtest當前的版本是1。5。0,如果使用VisualC++編譯,要求編譯器版本不低於7。1(VisualC++2003)。如下圖所示,它的msvc資料夾包含VisualC++工程和專案檔案,samples資料夾包含10個使用範例。

一般情況下,我們的單元測試程式碼只需要包含標頭檔案gtest。h。gtest中常用的所有結構體、類、函式、常量等,都通過名稱空間testing訪問,不過gtest已經把最簡單常用的單元測試功能包裝成了一些帶引數巨集,因此在簡單的測試中常常可以忽略名稱空間的存在。

按照gtest的叫法,巨集TEST為特定的測試用例(TestCase)定義了一個可執行的測試(Test)。它接受使用者指定的測試用例名(一般取被測物件名)和測試名作為引數,並劃出了一個作用域供填充測試巨集語句和普通的C++程式碼。一系列TEST的集合就構成一個簡單的測試程式。

常用的測試巨集如下表所示。以ASSERT_開頭和以EXPECT_開頭的巨集的區別是,前者在測試失敗時會給出報告並立即終止測試程式,後者在報告後繼續執行測試程式。

寫個簡單的測試試一下。假設我們實現了一個加法函式:

 對應的單元測試程式可以這樣寫:

程式碼中,測試用例Add包含兩個測試,正數和負數(這裡利用了VisualC++2005以上允許識別符號包含Unicode字元的特性)。編譯執行效果如下:

在控制檯介面中,通過的測試用綠色表示,失敗的測試用紅色表示。雙橫線分隔了不同的測試用例,其中包含的每個測試的啟動與結果用單橫線和RUN。。。OK或RUN。。。FAILED標出。失敗的測試會打印出程式碼行和原因,測試程式最後為所有用例和測試顯示統計結果。建議讀者試一下換成ASSERT_巨集的不同之處。

每個測試巨集還可以使用<<運算子在測試失敗時輸出自定義資訊,如:

編譯命令列中,gtest_mt。lib和gtest_main_mt。lib就是前面使用VC專案檔案生成的靜態庫。有意思的是,測試程式碼不需要註冊測試用例,也不需要定義main函式,這是gtest通過後一個靜態庫自動完成的,它的實現程式碼如下:

其中,函式InitGoogleTest負責註冊需要執行的所有測試用例,巨集RUN_ALL_TEST負責執行所有測試,如果全部成功則返回0,否則返回1。當然,我們也可以僅連結gtest_mt。lib,自己提供main函式。

二、測試韌體

很多時候,我們想在不同的測試執行前建立相同的配置環境,在測試執行結束後執行相應的清理工作,測試韌體(TestFixture)為這種需求提供了方便。“Fixture”是一個漢語中不易直接對應的詞,《美國傳統詞典》對它的解釋是“(作為附屬物的)固定裝置;被固定的狀態”。在單元測試中,Fixture的作用是為測試建立輔助性的上下文環境,實現測試的初始化和終結與測試過程本身的分離,便於不同測試使用相同程式碼來搭建固定的配置環境。用體操比賽的說法,測試過程體現了特定測試的自選動作,測試韌體則體現了對一系列測試(在開始和結束時)的規定動作。有些講單元測試的書籍直接把測試韌體稱為Scaffolding(腳手架)。

使用測試韌體比單純呼叫TEST巨集稍微麻煩一些:

1、從gtest的testing::Test類派生一個類,用public或protected定義以下所有成員。

2、(可選)建立環境:使用預設建構函式,或定義一個虛成員函式virtualvoidSetUp()。

3、(可選)銷燬環境:使用解構函式,或定義一個虛成員函式virtualvoidTearDown()。

4、用TEST_F定義測試,寫法與TEST相同,但測試用例名必須為上面定義的類名。

每個帶韌體的測試的執行順序是:

1、呼叫預設建構函式建立一個新的帶韌體物件。

2、立即呼叫SetUp函式。

3、執行TEST_F體。

4、立即呼叫TearDown函式。

5、呼叫解構函式銷燬類物件。

從gtest的實現程式碼可以看到,TEST_F又從使用者定義的類自動派生了一個類,因此要求public或protected的訪問許可權;大括號裡的內容被擴充套件成一個名為TestBody的虛成員函式的函式體,因此可以在其中直接訪問成員變數和成員函式。其實TEST也採用了相同的實現機制,只是它直接從gtest的testing::Test自動派生類,所以可以指定任意用例名。testing::Test類的SetUp和TearDown都是空函式,所以它只執行測試步驟,沒有環境的建立和銷燬。

借用上面Add函式寫個韌體測試的例子:

編譯執行效果如下:

必須強調,每個TEST_F開始都建立了一個新的帶韌體物件,因此每個測試都使用獨立的完全相同的初始環境,各測試可以按任意順序執行(參見--gtest_shuffle命令列選項)。但在某些情況下,我們可能需要在各個測試間共享一個相同的環境來儲存和傳遞狀態,或者環境的狀態是隻讀的,可以只初始化一次,再或者建立環境的過程開銷很高,要求只初始化一次。共享某個韌體環境的所有測試合稱為一個“測試套件”(TestSuite),gtest中利用靜態成員變數和靜態成員函式實現這個概念:

1、(可選)在testing::Test的派生類中,定義若干靜態成員變數來維護套件的狀態。

2、(可選)建立共享環境:定義一個靜態成員函式staticvoidSetUpTestCase()。

3、(可選)銷燬共享環境:定義一個靜態成員函式staticvoidTearDownCase()。

另外,還可以使用gtest的Environment類來建立和銷燬所有測試共用的全域性環境(對應於上圖顯示的“Globaltestenvironmentset-up”和“Globaltestenvironmenttear-down”):

gtest文件建議測試程式自己定義main函式並在其中建立和註冊全域性環境物件:

三、異常測試

C程式中要返回出錯資訊,可以利用特定的函式返回值、函式的輸出(outbound)引數、或者設定全域性變數(如C標準庫定義的errno,Windows API中的“上次錯誤”(last error)程式碼,Winsock中與每個socket相關聯的錯誤程式碼)。C++程式常用異常(exception)來返回出錯資訊,gtest為異常測試提供了專用的測試巨集:

需要注意,這些測試巨集都接受C/C++語句作為引數,所以既可以像前面那樣傳遞表示式,也可以傳遞用大括號包起來的程式碼塊。

藉助下面的被測函式:

測試程式如下:

編譯執行效果如下:

容易想到,gtest的這些異常測試巨集是用C++的try。。。catch語句來實現的:

如果把上圖中Visual C++的編譯選項/EHsc換成/EHa,try 。。。 catch就可以同時支援C++風格的異常和Windows系統的結構化異常(SEH)。這樣,即使刪掉divide函式裡的if判斷,測試程式碼的EXPECT_ANY_THROW巨集也會成功捕獲異常。

遺憾的是,目前僅使用這些測試巨集無法得到獲得被丟擲異常的詳細資訊(如divide函式中的報錯文字),這和gtest自身不願意使用C++異常有關。

四、值引數化測試

有些時候,我們需要對程式碼實現的功能使用不同的引數進行測試,比如使用大量隨機值來檢驗演算法實現的正確性,或者比較同一個介面的不同實現之間的差別。gtest把“集中輸入測試引數”的需求抽象出來提供支援,稱為值引數化測試(Value Parameterized Test)。

值引數化測試包括4個步驟:

1、從gtest的TestWithParam模板類派生一個類(記為C),模板引數為需要輸入的測試引數的型別。由於TestWithParam本身是從Test派生的,所以C就成了一個測試韌體類。

2、在C中,可以實現諸如SetUp、TearDown等方法。特別地,測試引數由TestWithParam實現的GetParam()方法依次返回。

3、使用TEST_P(而不是TEST_F)定義測試。

4、使用INSTANTIATE_TEST_CASE_P巨集集中輸入測試引數,它接受3個引數:任意的文字字首,測試類名(這裡即為C),以及測試引數值序列。gtest框架依次使用這些引數值生成測試韌體類例項,並執行使用者定義的測試。

gtest提供了專門的模板函式來生成引數值序列,如下表所示:

寫個小程式試一下。假設我們實現了一種快速累加演算法,希望使用另一種直觀演算法進行正確性校驗。演算法實現和測試程式碼如下:

測試程式如下:

注意TestWithParam的模板引數設定為unsigned型別,而在程式碼倒數第2行,兩個常量值都加了u字尾來指定為unsigned型別。熟悉C++的讀者應該知道,模板函式在進行型別推斷(deduction)時匹配相當嚴格,不像普通函式那樣允許型別提升(promotion)。如果上面省略u字尾,就會造成編譯錯誤。當然還可以顯式指定模板引數:testing::Range(1, 1000)。