C++開源單元測試框架googletest

Google C++ Testing Framework Primer

項目主頁:http://code.google.com/p/googletest/

Introduction:爲什麼需要Google C++ 測試框架?

Google C++ 測試框架幫助你更好地編寫C++測試。

無論你是在Linux,Windows,還是Mac環境下工作,只要你編寫C++代碼,Google 測試框架都可以幫上忙。

那麼,哪些因素才能構成一個好的測試?以及,Google C++ 測試框架怎樣滿足這些因素?我們相信:

  1. 測試應該是獨立可重複的。因爲其他測試成功或失敗而導致我們要對自己的測試進行debug是非常痛苦的。Google C++ 測試框架通過將每個測試在不同的對象中運行,使得測試分離開來。當一個測試失敗時,Google C++ 測試框架允許你獨立運行它以進行快速除錯。
  2. 測試應該能夠被很好地組織,並反映被測代碼的結構。Google C++ 測試框架將測試組織成測試案例,案例中的測試可以共享數據和程序分支。這樣一種通用模式能夠很容易辨識,使得我們的測試容易維護。當開發人員在項目之間轉換,開始在一個新的代碼基上開始工作時,這種一致性格外有用。
  3. 測試應該是可移植可重用的。開源社區有很多平臺獨立的代碼,它們的測試也應該是平臺獨立的。除開一些特殊情況,Google C++ 測試框架運行在不同的操作系統上、與不同的編譯器(gcc、icc、MSVC)搭配,Google C++ 測試框架的測試很容易與不同的配置一起工作。
  4. 當測試失敗時,應該提供儘可能多的、關於問題的信息。Google C++ 測試框架在第一個測試失敗時不會停下來。相反,它只是將當前測試停止,然後繼續接下來的測試。你也可以設置對一些非致命的錯誤進行報告,並接着進行當前的測試。這樣,你就可以在一次“運行-編輯-編譯”循環中檢查到並修復多個bug。
  5. 測試框架應該能將測試編寫人員從一些環境維護的工作中解放出來,使他們能夠集中精力於測試的內容。Google C++ 測試框架自動記錄下所有定義好的測試,不需要用戶通過列舉來指明哪些測試需要運行。
  6. 測試應該快速。使用Google C++ 測試框架,你可以重用多個測試的共享資源,一次性完成設置/解除設置,而不用使一個測試去依賴另一測試。

因爲Google C++ 測試框架基於著名的xUnit架構,如果你之前使用過JUnit或PyUnit的話,你將會感覺非常熟悉。如果你沒有接觸過這些測試框架,它也只會佔用你大約10分鐘的時間來學習基本概念和上手。所以,讓我們開始吧!

Note:本文偶爾會用“Google Test”來代指“Google C++ 測試框架”。

基本概念

使用Google Test時,你是從編寫斷言開始的,而斷言是一些檢查條件是否爲真的語句。一個斷言的結果可能是成功、非致命失敗,或者致命失敗。如果一個致命失敗出現,他會結束當前的函數;否則,程序繼續正常運行。

測試使用斷言來驗證被測代碼的行爲。如果一個測試崩潰或是出現一個失敗的斷言,那麼,該測試失敗;否則該測試成功

一個測試案例(test case)包含了一個或多個測試。你應該將自己的測試分別歸類到測試案例中,以反映被測代碼的結構。當測試案例中的多個測試需要共享通用對象和子程序時,你可以把他們放到一個測試固件(test fixture)類中。

一個測試程序可以包含多個測試案例。

從編寫單個的斷言開始,到創建測試和測試案例,我們將會介紹怎樣編寫一個測試程序。

斷言

Google Test中的斷言是一些與函數調用相似的宏。要測試一個類或函數,我們需要對其行爲做出斷言。當一個斷言失敗時,Google Test會在屏幕上輸出該代碼所在的源文件及其所在的位置行號,以及錯誤信息。也可以在編寫斷言時,提供一個自定義的錯誤信息,這個信息在失敗時會被附加在Google Test的錯誤信息之後。

斷言常常成對出現,它們都測試同一個類或者函數,但對當前功能有着不同的效果。ASSERT_*版本的斷言失敗時會產生致命失敗,並結束當前函數。EXPECT_*版本的斷言產生非致命失敗,而不會中止當前函數。通常更推薦使用EXPECT_*斷言,因爲它們運行一個測試中可以有不止一個的錯誤被報告出來。但如果在編寫斷言如果失敗,就沒有必要繼續往下執行的測試時,你應該使用ASSERT_*斷言。

因爲失敗的ASSERT_*斷言會立刻從當前的函數返回,可能會跳過其後的一些的清潔代碼,這樣也許會導致空間泄漏。根據泄漏本身的特質,這種情況也許值得修復,也可能不值得我們關心——所以,如果你得到斷言錯誤的同時,還得到了一個堆檢查的錯誤,記住上面我們所說的這一點。

要提供一個自定義的錯誤消息,只需要使用<<操作符,或一個<<操作符的序列,將其輸入到框架定義的宏中。下面是一個例子:

Cpp代碼

  1. ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length"; 
  2. for (int i = 0; i < x.size(); ++i) { 
  3. EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i; 
  4. }
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 編碼。

基本斷言

下面這些斷言實現了基本的true/false條件測試。

致命斷言
非致命斷言
驗證條件

ASSERT_TRUE(condition);
EXPECT_TRUE(condition);
condition爲真

ASSERT_FALSE(condition);
EXPECT_FALSE(condition);
condition 爲假

記住,當它們失敗時,ASSERT_*產生一個致命失敗並從當前函數返回,而EXCEPT_*產生一個非致命失敗,允許函數繼續運行。在兩種情況下,一個斷言失敗都意味着它所包含的測試失敗。

有效平臺: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

在出現失敗事件時,Google Test會將兩個值(Val1Val2)都打印出來。在ASSERT_EQ*和EXCEPT_EQ*斷言(以及我們隨後介紹類似的斷言)中,你應該把你希望測試的表達式放在actual(實際值)的位置上,將其期望值放在expected(期望值)的位置上,因爲Google Test的測試消息爲這種慣例做了一些優化。

參數值必須是可通過斷言的比較操作符進行比較的,否則你會得到一個編譯錯誤。參數值還必須支持<<操作符來將值輸入到ostream中。所有的C++內置類型都支持這一點。

這些斷言可以用於用戶自定義的型別,但你必須重載相應的比較操作符(如==、<等)。如果定義有相應的操作符,推薦使用ASSERT_*()宏,因爲它們不僅會輸出比較的結果,還會輸出兩個比較對象。

參數表達式總是隻被解析一次。因此,參數表達式有一定的副作用(side effect,這裏應該是指編譯器不同,操作符解析順序的不確定性)也是可以接受的。但是,同其他普通C/C++函數一樣,參數表達式的解析順序是不確定的(如,一種編譯器可以自由選擇一種順序來進行解析),而你的代碼不應該依賴於某種特定的參數解析順序。

ASSERT_EQ()對指針進行的是指針比較。即,如果被用在兩個C字符串上,它會比較它們是否指向同樣的內存地址,而不是它們所指向的字符串是否有相同值。所以,如果你想對兩個C字符串(例如,const char*)進行值比較,請使用ASSERT_STREQ()宏,該宏會在後面介紹到。特別需要一提的是,要驗證一個C字符串是否爲空(NULL),使用 ASSERT_STREQ(NULL, c_string)。但是要比較兩個string對象時,你應該使用ASSERT_EQ。

本節中介紹的宏都可以處理窄字符串對象和寬字符串對象(string和wstring)。

有效平臺:Linux、Windows、Mac。

字符串比較

該組斷言用於比較兩個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*也接受寬字符串(wchar_t*)。如果兩個寬字符串比較失敗,它們的值會做爲UTF-8窄字符串被輸出。

一個NULL空指針和一個空字符串會被認爲是不一樣的。

有效平臺:Linux、Windows、Mac。

參見:更多的字符串比較的技巧(如子字符串、前綴和正則表達式匹配),請參見[Advanced Guide Advanced Google Test Guide]。

簡單的測試

要創建一個測試:

  1. 使用TEST()宏來定義和命名一個測試函數,它們是一些沒有返回值的普通C++函數。
  2. 在這個函數中,與你想要包含的其它任何有效C++代碼一起,使用Google Test提供的各種斷言來進行檢查。
  3. 測試的結果由其中的斷言決定;如果測試中的任意斷言失敗(無論是致命還是非致命),或者測試崩潰,那麼整個測試就失敗了。否則,測試通過。

Cpp代碼

  1. TEST(test_case_name, test_name) { 
  2. ... test body ... 
  3. }
TEST(test_case_name, test_name) { ... test body ... }

TEST()的參數是從概括到特殊的。第一個參數是測試案例的名稱,第二個參數是測試案例中的測試的名稱。記住,一個測試案例可以包含任意數量的獨立測試。一個測試的全稱包括了包含它的測試案例名稱,及其獨立的名稱。不同測試案例中的獨立測試可以有相同的名稱。

舉例來說,讓我們看一個簡單的整數函數:

Cpp代碼

  1. int Factorial(int n); // 返回n的階乘
int Factorial(int n); // 返回n的階乘

這個函數的測試案例應該看起來像是:

Cpp代碼

  1. // 測試0的階乘
  2. TEST(FactorialTest, HandlesZeroInput) { 
  3. EXPECT_EQ(1, Factorial(0)); 
  4. // 測試正數的階乘
  5. TEST(FactorialTest, HandlesPositiveInput) { 
  6. EXPECT_EQ(1, Factorial(1)); 
  7. EXPECT_EQ(2, Factorial(2)); 
  8. EXPECT_EQ(6, Factorial(3)); 
  9. EXPECT_EQ(40320, Factorial(8)); 
  10. }
// 測試0的階乘 TEST(FactorialTest, HandlesZeroInput) { EXPECT_EQ(1, Factorial(0)); } // 測試正數的階乘 TEST(FactorialTest, HandlesPositiveInput) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); }

Google Test根據測試案例來分組收集測試結果,因此,邏輯相關的測試應該在同一測試案例中;換句話說,它們的TEST()的第一個參數應該是一樣的。在上面的例子中,我們有兩個測試,HandlesZeroInput和HandlesPostiveInput,它們都屬於同一個測試案例 FactorialTest。

有效平臺:Linux、Windows、Mac。

測試固件(Test Fixtures,又做測試夾具、測試套件):在多個測試中使用同樣的數據配置

當你發現自己編寫了兩個或多個測試來操作同樣的數據,你可以採用一個測試固件。它讓你可以在多個不同的測試中重用同樣的對象配置。

要創建測試固件,只需:

  1. 創建一個類繼承自testing::Test。將其中的成員聲明爲protected:或是public:,因爲我們想要從子類中存取固件成員。
  2. 在該類中聲明你計劃使用的任何對象。
  3. 如果需要,編寫一個默認構造函數或者SetUp()函數來爲每個測試準備對象。常見錯誤包括將SetUp()拼寫爲Setup()(小寫了u)——不要讓它發生在你身上。
  4. 如果需要,編寫一個析構函數或者TearDown()函數來釋放你在SetUp()函數中申請的資源。要知道什麼時候應該使用構造函數/析構函數,什麼時候又應該使用SetUp()/TearDown()函數,閱讀我們的FAQ。
  5. 如果需要,定義你的測試所需要共享的子程序。

當我們要使用固件時,使用TEST_F()替換掉TEST(),它允許我們存取測試固件中的對象和子程序:

Cpp代碼

  1. TEST_F(test_case_name, test_name) { 
  2. ... test body ... 
  3. }
TEST_F(test_case_name, test_name) { ... test body ... }

與TEST()一樣,第一個參數是測試案例的名稱,但對TEST_F()來說,這個名稱必須與測試固件類的名稱一些。你可能已經猜到了:_F正是指固件。

不幸地是,C++宏系統並不允許我們創建一個單獨的宏來處理兩種類型的測試。使用錯誤的宏會導致編譯期的錯誤。

而且,你必須在TEST_F()中使用它之前,定義好這個測試固件類。否則,你會得到編譯器的報錯:“virtual outside class declaration”。

對於TEST_F()中定義的每個測試,Google Test將會:

  1. 在運行時創建一個全新的測試固件
  2. 馬上通過SetUp()初始化它,
  3. 運行測試
  4. 調用TearDown()來進行清理工作
  5. 刪除測試固件。注意,同一測試案例中,不同的測試擁有不同的測試固件。Google Test在創建下一個測試固件前總是會對現有固件進行刪除。Google Test不會對多個測試重用一個測試固件。測試對測試固件的改動並不會影響到其他測試。

例如,讓我們爲一個名爲Queue的FIFO隊列類編寫測試,該類的接口如下:

Cpp代碼

  1. template <typename E> // E爲元素類型
  2. class Queue { 
  3. public: 
  4. Queue(); 
  5. void Enqueue(const E& element); 
  6. E* Dequeue(); // 返回 NULL 如果隊列爲空.
  7. size_t size() const; 
  8. ... 
  9. };
template <typename E> // E爲元素類型 class Queue { public: Queue(); void Enqueue(const E& element); E* Dequeue(); // 返回 NULL 如果隊列爲空. size_t size() const; ... };

首先,定義一個固件類。習慣上,你應該把它的名字定義爲FooTest,這裏的Foo是被測試的類。

Cpp代碼

  1. class QueueTest : public testing::Test { 
  2. protected: 
  3. virtual void SetUp() { 
  4. q1_.Enqueue(1); 
  5. q2_.Enqueue(2); 
  6. q2_.Enqueue(3); 
  7. // virtual void TearDown() {}
  8. Queue<int> q0_; 
  9. Queue<int> q1_; 
  10. Queue<int> q2_; 
  11. };
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()和這個固件來編寫測試。

Cpp代碼

  1. TEST_F(QueueTest, IsEmptyInitially) { 
  2. EXPECT_EQ(0, q0_.size()); 
  3. TEST_F(QueueTest, DequeueWorks) { 
  4. int* n = q0_.Dequeue(); 
  5. EXPECT_EQ(NULL, n); 

  6. n = q1_.Dequeue(); 
  7. ASSERT_TRUE(n != NULL); 
  8. EXPECT_EQ(1, *n); 
  9. EXPECT_EQ(0, q1_.size()); 
  10. delete n; 

  11. n = q2_.Dequeue(); 
  12. ASSERT_TRUE(n != NULL); 
  13. EXPECT_EQ(2, *n); 
  14. EXPECT_EQ(1, q2_.size()); 
  15. delete n; 
  16. }
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_TURE(n!= NULL),因爲我們隨後會n指針解引用,如果n指針爲空的話,會導致一個段錯誤。

當這些測試開始時,會發生如下情況:

  1. Google Test創建一個QueueTest對象(我們把它叫做t1)。
  2. t1.SetUp()初始化t1。
  3. 第一個測試(IsEmptyInitiallly)在t1上運行。
  4. 測試完成後,t1.TearDown()進行一些清理工作。
  5. t1被析構。
  6. 以上步驟在另一個QueueTest對象上重複進行,這回會運行DequeueWorks測試。

有效平臺:Linux、Windows、Mac。

注意:當一個測試對象被構造時,Google Test會自動地保存所有的Google Test變量標識,對象析構後進行恢復。

調用測試

TEST()和TEST_F()向Google Test隱式註冊它們的測試。因此,與很多其他的C++測試框架不同,你不需要爲了運行你定義的測試而將它們全部再列出來一次。

在定義好測試後,你可以通過RUN_ALL_TESTS()來運行它們,如果所有測試成功,該函數返回0,否則會返回1.注意RUN_ALL_TESTS()會運行你鏈接到的所有測試——它們可以來自不同的測試案例,甚至是來自不同的文件。

當被調用時,RUN_ALL_TESTS()宏會:

  1. 保存所有的Google Test標誌。
  2. 爲一個側測試創建測試固件對象。
  3. 調用SetUp()初始化它。
  4. 在固件對象上運行測試。
  5. 調用TearDown()清理固件。
  6. 刪除固件。
  7. 恢復所有Google Test標誌的狀態。
  8. 重複上訴步驟,直到所有測試完成。

此外,如果第二步時,測試固件的構造函數產生一個致命錯誤,繼續執行3至5部顯然沒有必要,所以它們會被跳過。與之相似,如果第3部產生致命錯誤,第4部也會被跳過。

重要:你不能忽略掉RUN_ALL_TESTS()的返回值,否則gcc會報一個編譯錯誤。這樣設計的理由是自動化測試服務會根據測試退出返回碼來決定一個測試是否通過,而不是根據其stdout/stderr輸出;因此你的main()函數必須返回RUN_ALL_TESTS()的值。

而且,你應該只調用RUN_ALL_TESTS()一次。多次調用該函數會與Google Test的一些高階特性(如線程安全死亡測試thread-safe death tests)衝突,因而是不被支持的。

有效平臺:Linux、Windows、Mac。

編寫main()函數

你可以從下面這個樣板開始:

Cpp代碼

  1. #include "this/package/foo.h"
  2. #include <gtest/gtest.h>
  3. namespace { 
  4. // 測試Foo類的測試固件
  5. class FooTest : public testing::Test { 
  6. protected: 
  7. // You can remove any or all of the following functions if its body
  8. // is empty.
  9. FooTest() { 
  10. // You can do set-up work for each test here.
  11. virtual ~FooTest() { 
  12. // You can do clean-up work that doesn't throw exceptions here.
  13. // If the constructor and destructor are not enough for setting up
  14. // and cleaning up each test, you can define the following methods:
  15. virtual void SetUp() { 
  16. // Code here will be called immediately after the constructor (right
  17. // before each test).
  18. virtual void TearDown() { 
  19. // Code here will be called immediately after each test (right
  20. // before the destructor).
  21. // Objects declared here can be used by all tests in the test case for Foo.
  22. }; 

  23. // Tests that the Foo::Bar() method does Abc.
  24. TEST_F(FooTest, MethodBarDoesAbc) { 
  25. const string input_filepath = "this/package/testdata/myinputfile.dat"; 
  26. const string output_filepath = "this/package/testdata/myoutputfile.dat"; 
  27. Foo f; 
  28. EXPECT_EQ(0, f.Bar(input_filepath, output_filepath)); 

  29. // Tests that Foo does Xyz.
  30. TEST_F(FooTest, DoesXyz) { 
  31. // Exercises the Xyz feature of Foo.
  32. } // namespace

  33. int main(int argc, char **argv) { 
  34. testing::InitGoogleTest(&argc, argv); 
  35. return RUN_ALL_TESTS(); 
  36. }
#include "this/package/foo.h" #include <gtest/gtest.h> namespace { // 測試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()函數負責解析命令行傳入的Google Test標誌,並刪除所有它可以處理的標誌。這使得用戶可以通過各種不同的標誌控制一個測試程序的行爲。關於這一點我們會在GTestAdvanced中講到。你必須在調用RUN_ALL_TESTS()之前調用該函數,否則就無法正確地初始化標示。

在Windows上InitGoogleTest()可以支持寬字符串,所以它也可以被用在以UNICODE模式編譯的程序中。

進階閱讀

恭喜你!你已經學到了一些Google Test基礎。你可以從編寫和運行幾個Google Test測試開始,再閱讀一下GoogleTestSamples,或是繼續研究GoogleTestAdvancedGuide,其中描述了很多更有用的Google Test特性。

已知侷限

Google Test被設計爲線程安全的。但是,我們還沒有時間在各種平臺上實現同步原語(synchronization primitives)。因此,目前從兩個線程同時使用Google Test斷言是不安全的。由於通常斷言是在主線程中完成的,因此在大多數測試中這都不算問題。如果你願意幫忙,你可以試着在gtest-port.h中實現必要的同步原語。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章