原始鏈接:V1_6_Primer
注
- GTest或者Google Test: Google的C++測試框架。
- Test Fixtures: 這個詞實在找不到對應的中文。
- Bug: 太常用了,不翻譯。
- House keeping chores: 家常事務。指非核心的編碼工作,比如測試代碼的assert, log以及用例管理等工作。
- set-up/tear-down: 指運行測試前的准備和之后的清理工作。
- test case: 測試用例,管理測試的單位,一個測試用例可以包含一個或多個測試。
在閱讀之前,推薦閱讀《為什么有如此多的C++測試框架 - from Google Testing Blog》了解一些背景知識。
目錄
GTest幫助你寫更好的C++測試代碼。
不管你在什么平台上工作,無論是Linux,Windows還是Mac,只要你使用C++,GTest就可以幫助你。
對於什么是一個好的測試,GTest如何來幫助實現這個目標,我們的觀點如下:
- 測試必須是獨立並且可重復的。如果某個測試的通過還是失敗依賴於其它測試的執行結果,那么調試它將是非常困難的。GTest通過在不同的項目下分別執行測試使得它們相互隔離。當某個測試失敗后,GTest允許你單獨執行它以便快速調試。
- 測試的組織必須很好的反映測試代碼的結構。GTest以測試用例為單位把那些能共享相同數據和代碼的測試組織起來。這種常見的模式使得我們可以很方便的識別和維護測試。當人們切換項目在新的代碼基礎上工作時,這種一致性會顯得特別有幫助。
- 測試必須可移植和可重用。開源社區的很多代碼都是平台中立的,所以它們的測試也必須平台中立。GTest可以在不同的操作系統上工作,使用不同的編譯器,支持或不支持異常,所以GTest可以很容易的在不同配置上工作。(注意:當前的發行版只包含Linux的構建腳本,我們正努力工作以支持其它平台。)
- 一個測試失敗后必須提供盡可能多的提示信息。GTest不會因為一個失敗就停止繼續工作。它僅僅停止當前測試並且繼續執行后續的測試。你還可以設置測試在遇到非致命失敗時繼續執行並打印相關信息。通過這種方法,你可以在一個“執行-編輯-編譯”周期中發現和修復多處bug。
- 測試框架必須把測試編寫者從繁重的家常事務中解脫出來以集中精力於測試本身。GTest自動維護和跟蹤所有定義的測試,不需要你為了運行測試而去手工枚舉所有測試。
- 測試必須迅速。使用GTest,你可以在不同測試之間使用共享的資源,而只需要做一次set-up/tear-down的工作,並且測試和測試之間相互獨立。
因為GTest只用流行的xUnit架構,只要你熟練使用JUnit或PyUnit就一定會感到非常親切。如果以前沒玩過的話,只要花10分鍾就可以上手了。好吧,我們繼續!
使用GTest編寫測試程序,你必須先把GTest編譯成一個庫文件然后在你的測試程序中鏈接它。我們為主流的構建系統准備了一些現成的構建腳本:GTest根目錄下的msvc/用於Visual Studio,xcode/用於Mac的Xcode,make/用於GNU make,codegear/用於Borland C++ Builder,另外還有autotools script(已淘汰)和CMakeLists用於CMake(推薦的)。如果你的構建系統不在以上列表中,你可以參考make/Makefile文件學習GTest是如何被編譯的(一般來說編譯src/gtest-all.cc,把GTEST_ROOT和GTEST_ROOT/include加到頭文件搜索路徑中,GTEST_ROOT指GTest根目錄)。
使用GTest你肯定會接觸到斷言這個概念。斷言是用來判斷某個條件是否為真。一個斷言的結果可以是成功,也可以是非致命失敗或致命失敗。如果發生了一個致命失敗,當前函數就會退出,不然程序還是會繼續正常執行。
測試使用斷言來判斷測試代碼的行為。如果測試崩潰了或者斷言失敗,那么這個測試就失敗了,不然就是通過。
一個測試用例包含一個或多個測試。你必須用測試用例把你的測試進行分組以反映測試代碼的結構。當某個測試用例中的多個測試共享一些對象或程序時,你可以把這些對象和程序放進test fixture類。
一個測試程序可以包含多個測試用例。
我們現在開始講解如何編寫一個測試程序,先從單個斷言開始,然后逐步到測試和測試用例。
GTest的斷言使用宏來組合一組函數調用。你通過對某個行為實施斷言的結果來測試一個類或者函數。如果斷言失敗,GTest打印斷言所在的文件和行數,以及失敗信息。你還可以在GTest標准的輸出信息之外添加自定義的失敗信息。
對一個行為實施斷言可能產生不同的影響。ASSERT_*版本在失敗時會產生致命失敗並退出當前函數。而EXPECT_*版本則產生一個非致命失敗,而且不會退出當前函數。一般來說我們更傾向使用EXPECT_*版本的斷言,因為這使得我們在一個測試中可以報告多個失敗。但是如果失敗后執行程序變得沒有意義,那么你就該使用ASSERT_*版本。
因為ASSERT_*版本的斷言在失敗后立即從當前函數返回,所以可能會因為跳過清理代碼導致資源泄露。根據泄露的性質,你也許需要或不需要修正這個問題。但無論如何牢記你有可能因為斷言錯誤導致額外的堆檢查錯誤(heap checker error)。
簡單的只用流定向操作符"<<"你就可以提供定制的失敗信息。
ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length"; for (int i = 0; i < x.size(); ++i) { EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i; }
任意能輸出到ostream流的內容就能被輸出到斷言,特別是C的字符串和string對象。如果是寬字符(wchar_t*,Windows下UNICODE模式的TCHAR*,或者std::wstring)內容被輸出到斷言,它們在打印時會自動被轉換成UTF-8編碼。
以下斷言用於基本的真/假條件測試。
致命斷言 | 非致命斷言 | 通過條件 |
ASSERT_TRUE() | EXPECT_TRUE(condition) | condition為真 |
ASSERT_FALSE() | EXPECT_FALSE(condition) | condition為假 |
記住,當失敗的時候,ASSERT_*會產生一個致命失敗並從當前函數返回,而EXPECT_*則產生一個非致命失敗,並允許程序繼續執行。在任何一種情況下,斷言失敗就意味着測試失敗。
在Linux,Windows和Mac上可用。
這一節列出了用於比較兩個值的斷言。
致命斷言 | 非致命斷言 | 通過條件 |
ASSERT_EQ(expected, actual) | EXPECT_EQ(expected, actual) | expected == actual |
ASSERT_NE(val1, val2) | EXPECT_NE(val1, val2) | val1 != val2 |
ASSERT_LT(val1, val2) | EXPECT_LT(val1, val2) | val1 < val2 |
ASSERT_LE(val1, val2) | EXPECT_LE(val1, val2) | val1 <= val2 |
ASSERT_GT(val1, val2) | EXPECT_GT(val1, val2) | val1 > val2 |
ASSERT_GE(val1, val2) | EXPECT_GE(val1, val2) | val1 >= val2 |
在失敗事件中,GTest會打印出val1和val2的值。在ASSERT_EQ*和EXPECT_EQ*以及其它相等性判斷斷言中,你應該把你要測試的表達式放在actual的位置上,把期望的值放在expected的位置上,因為GTest的失敗信息會據此規范進行優化。
值類型必須是斷言比較操作符能夠處理的類型,不然就會遇到編譯錯誤。我們以前要求參數支持"<<"操作符一邊輸出到ostream,但是從v1.6.0開始就不再必要了(如果支持"<<"操作符,當斷言失敗時就會被調用來打印參數,否則GTest就會自動嘗試用最好的方式打印參數。關於更多如何自定義打印參數的方法,請參考這篇關於Google Mock的文章。)。
這些斷言也可以和用戶自定義類型一起工作,但是你必須為這些類型定義對應的比較操作符(例如"==","<"等)。如果以上操作符被定義,最好使用ASSERT_*()系列的宏因為它們不僅打印比較的結果,而且還打印兩個操作數。
參數永遠只被計算一次。也就數說,參數有副作用也沒關系。但是和所有普通的C++函數一樣,參數的計算順序是不固定的(編譯器決定),所以你的代碼不能依賴於任何特定的參數求值順序。
ASSERT_EQ()不比較指針的相等性。如果你比較兩個C風格的字符串,它只比較它們是否在相同的內存位置,而不是它們的內容是否相同。所以,如果如果你想比較C風格的字符串(例如const char*)的值,就應該調用ASSERT_STREQ(),馬上就會被介紹到。特別注意,如果你想判斷C風格的字符串是不是一個空指針,請使用ASSERT_EQ(NULL, c_string)。但是,如果比較兩個string對象,請使用ASSERT_EQ。
以下斷言是用來比較兩個C風格字符串的。如果你想比較兩個string對象,請使用EXPECT_EQ,EXPECT_NE及其它類似函數。
致命斷言 | 非致命斷言 | 通過條件 |
ASSERT_STREQ(expected_str, actual_str) | EXPECT_STREQ(expected_str, actual_str) | 兩個C風格字符串內容相等 |
ASSERT_STRNE(str1, str2) | EXPECT_STRNE(str1, str2) | 兩個C風格字符串內容不相等 |
ASSERT_STRCASEEQ(expected_str, actual_str) | EXPECT_STRCASEEQ(expected_str, actual_str) | 忽略大小寫,兩個C風格字符串內容相等 |
ASSERT_STRCASENE(str1, str2) | EXPECT_STRCASENE(str1, str2) | 忽略大小寫,兩個C風格字符串內容不相等 |
注意斷言里的"CASE"表示大小寫將被忽略。
*STREQ*和*STRNE*也接受C風格的寬字符串(wchar_t*)。如果兩個寬字符串比較失敗,它們的值將使用UTF-8窄字符編碼被打印輸出。
一個空指針和一個空字符串被認為是不相等的。
在Linux,Windows和Mac上可用。
請參考《GTest高級指南》(翻譯施工中)來獲得更多關於字符串比較的技巧(子字符串,前綴,后綴,正則表達式匹配等)。
如何創建一個新的測試:
- 使用TEST()宏來定義和命名一個測試函數。它們是普通的C函數並且不返回任何值。
- 這個函數里你可以編寫任何有效的C++語句,並且使用不同的GTest斷言來對值做檢查。
- 測試結果由斷言來決定;如何任何一個斷言(致命或非致命)失敗,或者測試崩潰了,那么整個測試就失敗了,不然就算成功。
TEST(test_case_name, test_name) {
... test body ...
}
我們現在由淺入深講一下TEST()的參數。第一個參數是測試用例的名字,第二個是測試用例中具體測試的名字。兩個名字都必須是有效的C++標示符,並且不能包含下划線'_'。一個測試的完成名字包含測試用例的名字和它自己的名字。不同測試用例中的測試可以使用相同的名字。
我們以下面這個返回int的函數為例:
int Factorial(int n); // Returns the factorial of n
這個函數的測試用例可能如下代碼所示:
// Tests factorial of 0. TEST(FactorialTest, HandlesZeroInput) { EXPECT_EQ(1, Factorial(0)); } // Tests factorial of positive numbers. TEST(FactorialTest, HandlesPositiveInput) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }
GTest根據test cases把測試結果分組,所以邏輯上等價的測試必須放在同一個測試用例下。也就是說,它們TEST()宏的第一個參數必須相等。在上面的例子中,測試HandleZeroInput和HandlePosotiveInput都屬於同一個測試用例FactorialTest。
在Linux,Windows和Mac上可用。
Test Fixtures: 在多個測試中使用相同的數據配置
當你發現你編寫的多個測試只需要操作相似的數據,你也許應該考慮使用test fixture。這使得你可以在不同的測試中重用相同的數據配置。
創建一個fixture很簡單,只要以下幾步:
- 從::testing::Test集成一個類。根據我們希望訪問子類的fixture成員的權限,限定為protected或public。
- 在類中,定義任何你想使用的對象。
- 如果需要,實現默認的構造函數或SetUp()函數來為每個測試准備數據。一個常見的錯誤是把SetUp()拼寫為Setup,千萬注意不要把大寫的'U'寫成小寫的'u'啊。
- 如果需要,實現析構函數和TearDown函數來釋放SetUp()函數分配的資源。要學習什么時候應該使用構造/析構函數和什么時候使用SetUp()/TearDown()函數,請參考這份FAQ。
- 如果需要,請自定義測試需要共享的函數。
如果想使用fixture,請使用TEST_F()代替TEST()以便訪問test fixture的數據和函數。
TEST_F(test_case_name, test_name) {
... test body ...
}
和TEST()的第一個參數用來表示測試名字類似,TEST_F()的第一個名字用來表示test fixture的名字。你也許立刻就猜到了,"_F"用來表示fixture。
另外,在使用TEST_F()前,你要先定義一個test fixture類,不然你就會得到一個編譯錯誤"virtual outside class declaration"。
對於每一個被TEST_F()定義的測試,GTest會做以下工作:
- 在運行時創建一個新的test fixture。
- 立即調用SetUp()進行初始化。
- 運行測試。
- 調用TearDown進行清理。
- 銷毀test fixture。注意相同測試用例下的不同測試使用不同的test fixture對象,GTest總是在創建新的對象前銷毀已有的。GTest不會為不同的測試重用相同的test fuxture對象。這樣做的好處在於一個測試對fixture做的改動不會影響其它測試。
現在我們來看一個例子,為一個名為Queue的先進先出隊列類寫一個測試,類的接口如下:
template <typename E> // E is the element type. class Queue { public: Queue(); void Enqueue(const E& element); E* Dequeue(); // Returns NULL if the queue is empty. size_t size() const; ... };
首先定義一個fixture類,命名為QueueTest。
class QueueTest : public ::testing::Test { protected: virtual void SetUp() { q1_.Enqueue(1); q2_.Enqueue(2); q2_.Enqueue(3); } // virtual void TearDown() {} Queue<int> q0_; Queue<int> q1_; Queue<int> q2_; };
在這個例子中我們沒有實現TearDown函數,因為不需要做清理工作,析構函數已經可以解決所有問題。
現在我們開始用TEST_F()來實現測試代碼。
TEST_F(QueueTest, IsEmptyInitially) { EXPECT_EQ(0, q0_.size()); } TEST_F(QueueTest, DequeueWorks) { int* n = q0_.Dequeue(); EXPECT_EQ(NULL, n); n = q1_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(1, *n); EXPECT_EQ(0, q1_.size()); delete n; n = q2_.Dequeue(); ASSERT_TRUE(n != NULL); EXPECT_EQ(2, *n); EXPECT_EQ(1, q2_.size()); delete n; }
上面我們同時使用了ASSERT_*和EXPECT_*斷言。規則是如果我們想通過斷言暴露盡可能多的錯誤,使用EXPECT_*。如果斷言失敗之后代碼繼續執行沒有意義,使用ASSERT_*。例如,Dequeue測試第二個斷言使用"ASSERT_TRUE(n != NULL)",因為我們需要在后面對指針解引用,所以如果它為空的話繼續執行就沒有任何意義。
當一個測試執行時會發生以下事件:
- GTest構造QueueTest對象(我們稱之為t1)。
- t1.SetUp()函數初始化t1。
- t1的第一個測試IsEmptyInitially執行。
- 在測試執行完后調用t1.TearDown()清理現場。
- t1被析構。
- 以上步驟重復一遍,這一輪是執行測試DequeueWorks。
在Linux,Windows和Mac上可用。
注:GTest在創建測試對象時自動保存所有GTest的標志位(flag),在對象銷毀后自動恢復這些標志位。
TEST()和TEST_F()默認向GTest注冊它們的測試。所以和其它C++測試框架不同,你不必為了執行測試而把你定義的測試重新列一遍。
在定義完你的測試后,你可以調用RUN_ALL_TESTS()來執行所有的測試。返回值為0說明全部測試通過,1則說明有失敗的測試。注意,RUN_ALL_TESTS()執行你鏈接在代碼里的所有測試,可以來自不同的測試用例,也可以來自不同的源文件。
當被調用時,RUN_ALL_TESTS()宏會執行以下任務:
- 保存GTest當前標志位的所有狀態。
- 為第一個測試創建新的test fixture對象。
- 調用SetUp()進行初始化。
- 執行測試。
- 調用TearDown()進行現場清理。
- 銷毀fixture對象。
- 重置之前保存的GTest的所有標志位。
- 重復以上步驟直到完成所有測試執行。
另外,如果第2步在test fixture的構造函數出現致命錯誤的話,第3到5步將被跳過。如果第3步出現致命錯誤的話,第4步將被跳過。
重要:千萬不要忽略RUN_ALL_TESTS()的返回值,不然gcc會給你一個編譯錯誤。設計時我們決定自動化測試服務根據退出代碼判斷測試是否通過,而不是stdout/stderr的輸出,所以main()函數必須返回RUN_ALL_TESTS()的返回值。
另外你只能調用RUN_ALL_TESTS()一次。多次調用將導致和GTest的一些高級特性沖突(線程安全的死亡測試)並導致這些特性無法正常工作。
在Linux,Windows和Mac上可用。
你可以先從這個模板文件起步:
#include "this/package/foo.h" #include "gtest/gtest.h" namespace { // The fixture for testing class Foo. class FooTest : public ::testing::Test { protected: // You can remove any or all of the following functions if its body // is empty. FooTest() { // You can do set-up work for each test here. } virtual ~FooTest() { // You can do clean-up work that doesn't throw exceptions here. } // If the constructor and destructor are not enough for setting up // and cleaning up each test, you can define the following methods: virtual void SetUp() { // Code here will be called immediately after the constructor (right // before each test). } virtual void TearDown() { // Code here will be called immediately after each test (right // before the destructor). } // Objects declared here can be used by all tests in the test case for Foo. }; // Tests that the Foo::Bar() method does Abc. TEST_F(FooTest, MethodBarDoesAbc) { const string input_filepath = "this/package/testdata/myinputfile.dat"; const string output_filepath = "this/package/testdata/myoutputfile.dat"; Foo f; EXPECT_EQ(0, f.Bar(input_filepath, output_filepath)); } // Tests that Foo does Xyz. TEST_F(FooTest, DoesXyz) { // Exercises the Xyz feature of Foo. } } // namespace int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
::testing::InitGoogleTest()函數解析從命令行輸入的GTest標志位,讀取完成后把以識別的標志位從命令行中刪除。這使得用戶可以使用不同的標志位控制程序的行為,在《GTest高級指南》(翻譯施工中)我們會做進一步討論。在調用RUN_ALL_TEST()之前你必須調用這個函數,不然標志位不會被正確初始化。
在Windows平台上,InitGoogleTest()可以和寬字符串一起工作,所以可以和程序一起用UNICODE模式編譯。
也許你會認為寫main()函數太麻煩了,我們完全同意,所以在GTest里提供了一個main()函數的基本實現。如果你覺得這個main函數夠用了,編譯時請鏈接gtest_main庫。
如果你把測試放在一個庫里,而main函數在你的.exe文件的另一個庫里,那么這些測試不會被執行。這是Visual C++的一個bug。當你定義測試時,GTest會創建某種靜態對象來注冊它們。這些對象不會在其它地方被引用但是它們的構造函數理論上還是會運行。但是Visual C++的linker看到這些庫里的對象沒有被任何地方引用時,它就把這個庫扔掉了。所以你必須在你的主程序中引用你庫里的測試,這樣linker就不會忽略它們了。這里是具體做法。在你的庫代碼中加上這么一段:
__declspec(dllexport) int PullInMyLibrary() { return 0; }
如果你使用靜態鏈接庫的話, __declspec(dllexport)可以省略。現在在你的主程序里加上這么一段代碼:
int PullInMyLibrary(); static int dummy = PullInMyLibrary();
這樣你的測試就會被引用,而且在程序開始的時候就會被注冊。
另外,如果你在靜態鏈接庫中定義測試,請在你的主程序鏈接選項中加入"/OPT:NOREF"。如果你使用MSVC++ IDE,請在你的.exe項目properties/Configuration Properties/Linker/Optimization的引用設置中選中Unreferenced Data(/OPT:NOREF)。這樣可以阻止Visual C++的linker在最終生成的可執行文件中忽略掉與你的測試相關的符號。
還有另外一個缺點。如果你把GTest編譯為靜態庫(在gtest.vcproj中就是這么定義的),那么你的測試也必須存在於靜態庫中。如果你使用動態鏈接庫DLL的話,你必須把GTest也編譯為動態鏈接庫。不然你的測試就不會運行或運行錯誤。一個簡單的結論是:想要你的生活更容易,不要把你的測試放在單獨的庫里。
恭喜!你已經掌握GTest的基本知識了。現在你可以開始用GTest編寫和運行你自己的測試,接下來,可以參考更多例子,或者探索《GTest高級指南》(翻譯施工中)以發現更多有用的特性。
GTest被設計成線程安全的。如果系統提供pthreads庫的話,那么實現就是線程安全的。目前在兩個並發的線程同時使用斷言在某些系統(比如Windows)上是不安全的。對大多數測試來說這不是一個問題,因為大多數斷言在主線程內完成工作。如果你願意幫助,你可以志願為你的平台在gtest-port.h里實現必要的同步原語。