適合於跨平臺的C++測試工具

Just4life發表於2013-08-23

gtest,英文全稱是Google C++ Testing Framework,英文簡稱是Google Test,中文譯為“谷歌C++測試框架”,它是從谷歌內部誕生並受到業界追捧的一個非常優秀的測試框架,支援如自動發現測試、自定義斷言、死亡測試、自動報告等諸多功能。

其他著名的自動化測試框架產品還有CppUnit、CxxTest、JUnit、PyUnit等。

如果你是一名開發工程師,或者你編寫的程式要用到生產環境中,那麼,你不可避免的需要學習和掌握一種自動化測試框架,以確保你的程式測試充分,質量上乘。

gtest官網教程原文,在這裡

【介紹:為什麼要選擇谷歌C++測試框架】

因為:“谷歌C++測試框架可以幫助你編寫出更好的C++測試程式”。

無論你的開發是基於Linux、Windows還是Mac,只要你使用的是C++語言,gtest都能夠幫助到你。

那麼,到底什麼才是好的測試,gtest又如何實現這種好的測試的呢?我們是這樣認為的:

  • 1. 測試應該是獨立的且可重複的。

(如果一個測試的結果是依賴於另一個測試的結果的,將是件很痛苦的事情。而gtest可以有效的避免這一點,它會確保每一個測試以一個獨立物件的形式存在。當一個測試失敗時,gtest支援你在獨立的環境中進行除錯。)

  • 2. 應該有一套方法較好的來組織我們的測試,這種組織方法要能夠較好地反映程式程式碼的結構。

(gtest會將test分組到“test case”中這樣可以很好的來組織和管理所有的測試了。同時,test cases之間既可以共享資訊,也可以巢狀。這種組織規則,會非常有利於記憶和管理。如果所有專案的測試都採用一致的組織規則,那麼人員在測試專案間的遷移成本也會大大降低。)

  • 3. 測試應該是可遷移的且可複用的。

(開源社群中有很多的程式碼是“平臺中立的”,也就是相容多種平臺,因而,這些程式碼的測試也應該遵循“平臺中立”的原則。基於這種考慮,gtest支援多種作業系統平臺、多種編譯器,所以,gtest可以很好的支援這類測試工作。)

  • 4. 在測試失敗時,要能夠提供足夠充分的測試資訊。

(gtest並不會在首次失敗後就停止工作,取而代之的是,gtest會停止當前這個測試,繼續下一個測試。當然,你完全可以設定讓gtest在繼續下一個測試的同時,輸出這次測試中非致命失敗的相關資訊,這樣,你就可以在一個測試周期中,偵測和修復更多個bugs。)

  • 5. 測試框架應該讓開發者從瑣碎重複的工作中解脫出來,讓它們能專注在測試內容上。

(gtest會自動的掃描和跟蹤所有定義的測試,而不會讓開發者一個一個去列舉。)

  • 6. 測試應該是高效的。

(使用gtest,你可以複用不同測試中的資源,另外,set-up/tear-down也支援“一處定義,多處複用”的特性。)

由於gtest是基於xUnit框架設計實現的,所以如果你之前使用過JUnit或PyUnit的話,你會很容易上手;否則,你或許需要花上10分鐘的時間來學習下相關的基礎知識。

好了,我們現在就開始!

【編譯gtest】

為了使用gtest來寫一個測試程式,你首先需要做的便是把gtest編譯成一個函式庫,並且連結到你的測試程式中。我們支援多種流行的build系統,如用於Visual Studio的msvc,用於Mac Xcode的xcode,,用於GNU make的make,用於Borland C++ builder的codegear,以及用於CMake的CMakeLists.txt。

如果很不幸,你所使用的build系統不在上述列表中,你可以下載make/Makefile來了解gtest的編譯方法,試著自己來編譯gtest。

在你編譯你自己的測試專案時,你的測試程式要引用gtest/gtest.h標頭檔案(假如你的gtest安裝在GTEST_ROOT路徑下,那麼gtest/gtest.h會存放在GTEST_ROOT/include資料夾下面)。

【基本概念】

當你使用gtest時,你會以assertion(斷言)開始,assertion用來檢查一個條件是否為真。一個assertion的結果可以為success(成功)、nonfatal failure(非致命失敗)和fatal failure(致命失敗)。一旦fatal failure發生,當前的測試函式會終止,而如果只是nonfatal failure,則測試程式還是會繼續執行的。

gtest就是使用assertion來驗證程式程式碼的行為的。如果一個測試崩潰或者assertion失敗,那麼測試就未通過。

一個test case可以包括一個或多個test。你需要把各種test歸類到你的test case中,這樣有利於更好的顯示出程式碼結構。當同一個test case中的多個test需要共享一些資訊時,你可以把這些test放到一個test fixture類中。
(大棚:或許你讀到這句話時,感覺有些晦澀,沒關係。test fixture的具體用法後面還會具體講的。:) )

一個test program往往會包含多個test case。

基本概念就只有這些,應該不難理解的。下面我們就來編寫第一個test program,主要是讓大家瞭解assertion的使用!

【初識斷言】

gtest的assertion本質上是一些巨集。

當一個assertion失敗了,gtest會顯示出這個assertion的原始檔名稱、所在行號以及錯誤資訊。當然,你也可以自定義錯誤資訊。

assertion在測試一個函式時,可以有兩種方案,即ASSERT_*和EXPECT_*。在失敗發生時,ASSERT_*這類assertion會產生fatal failure,並且會終止當前函式;而EXPECT_*則只會產生nonfatal failure。
(大棚:你還記得吧,fatal failure會引起函式終止,而nonfatal failure則不會)

我們通常推薦大家使用EXPECT_*類的assertion,因為它允許我們在一個測試周期中經歷多一些的failure。如果某個錯誤會導致後面的邏輯無法正常執行的話,那就只能用ASSERT_*來終止函式了。

如果你想自定義failure message,可以使用<<操作符,舉例如下:

1
2
3
4
5
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;
}

任何可以作為流的物件都可以輸出給assertion的巨集,包括C字元陣列及C++字串物件等,如果將寬字串(wchar_t*, TCHAR*, std::wstring)輸出給assertion,則會以UTF-8編碼方式輸出。

【斷言 – 基本用法】

下面這些assertion用來進行最基本的true/false條件判斷:

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false

還是要再提醒一下,當上述assertion失敗時,ASSERT_*會產生fatal failure並且立即從當前函式退出;而EXPECT_*只會產生nonfatal failure並且允許函式繼續向下執行。

【斷言 – 兩值比較】

我們講解一下如何通過assertion來做兩個值的比較。

Fatal assertion Nonfatal assertion Verifies
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

一旦assertion失敗,gtest會列印出val和val2的值。

而在ASSERT_EQ(expected, actual)和EXPECT_EQ(expected, actual)中,你應該在actual引數位置放置你想測試的表示式,而把你期望的值放在expected引數位置。

值得注意的是,你所提供得val1和val2應該是可以被比較的,否則gtest會報出編譯錯誤。上述這些assertion是支援使用者自定義型別的,但是要求你進行運算子過載(==、<、>等)。

對於C/C++函式來說,不同編譯器對引數檢查順序的規則是不同的,所以你的測試程式碼也不應該對此做任何假設。

ASSERT_EQ()在進行指標比較時需要格外注意。如果所傳入的是兩個C字元數串,assertion只會檢查兩個指標是否指向同一塊記憶體區域,而非檢查連個字串內容是否相同。所以,如果你想檢查兩個字串的內容是否相同,就要使用ASSERT_STREQ()巨集來做。例如,如果你想判斷C字元陣列是否是否為NULL,則可以使用“ASSERT_STREQ(NULL, c_string);”實現。不過,如果你想判斷兩個C++字串物件的話,則需要使用ASSERT_EQ()巨集。

本小節中涉及的assertion巨集,都支援窄字元物件(string)和寬字元物件(wstring)。

【斷言 – 字串比較】

本小節所講的巨集用來支援C字串的比較。如果你相比較的是兩個字串物件,那麼請使用
EXPECT_EQ/EXPECT_NE等巨集。

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(expected_str, actual_str); EXPECT_STREQ(expected_str, actual_str); the two C strings have the same content
ASSERT_STRNE(str1, str2); EXPECT_STRNE(str1,str2); the two C strings have different content
ASSERT_STRCASEEQ(expected_str, actual_str); EXPECT_STRCASEEQ(expected_str, actual_str); the two C strings have the same content,ignore case
ASSERT_STRCASENE(str1, str2); EXPECT_STRCASENE(str1,str2); the two C strings have different content,ignore case

需要注意的是,assertion巨集裡,“CASE”表示“忽略大小寫”。

STREQ和STRNE類巨集,也支援寬字串,並且在必要時(如字串比較失敗時),會以UTF-8窄字串
方式輸出。

另外,一個NULL和一個空字串被認為是不同的。

如果你想了解更多有關字串比較的trick方法,比如在assertion中處理子字串、字首、字尾、正則匹配等,請進入“高階gtest指南”。

【最簡單的測試】

為了建立一個test,你需要做三件事兒:

  • 1. 使用TEST()巨集來定義和命名一個test函式,這個test函式不需要return任何值。
  • 2. 在這個test函式中,你可以寫任何C++語句,並且使用assertion來檢查。
  • 3. 這個test的結果是由assertion決定的。如果任何一個assertion失敗了,或者這個test函式崩潰了,這個test則會返回fail。否則,會返回success。

編寫TEST()巨集的語法如下:

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

TEST()所需的兩個引數,從巨集觀到具體。第一個參數列示這個test所屬test case的名稱,第二個參數列示這個test自身的名稱。 名稱中不允許使用下劃線。

一個test的全稱,應該包括其所在的test case名稱及自身名稱。位於不同test case中的test可以擁有相同的名字。

舉例,讓我們來看一個簡單的階乘函式:

1
int Factorial(int n); // Returns the factorial of n

針對這個函式的test case,可以這樣來寫:

1
2
3
4
5
6
7
8
9
10
11
12
// 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 case來管理所有test,所以邏輯上相關的test應該放到同一個test case中,也就是說,TEST()的第一個引數應該相同。在上面這個例子中,我們建立了兩個test,即HandlesZeroInput和HandlesPositiveInput,它們兩個同屬FactorialTest這個test case。

【Test Fixtures】

如果你發現你所寫的多個test都在操作類似的資料,那麼我推薦你使用test fixture。這個特性允許你在不同的test裡複用相同的配置。

要想建立一個fixture,請遵循下面的步驟:

  • 1. 建立一個類,並繼承::testing::Test,並且使用protected或public限制符,以便其子類可以訪問到共享的資料。
  • 2. 在這個類中,宣告你想複用的物件。
  • 3. 如果有必要,請寫一個預設的建構函式或SetUp函式來準備所需物件。(要注意的是,不要將SetUp寫成Setup)
  • 4. 如果有必要,請寫一個解構函式或TearDown函式來釋放資源。
  • 5. 如果需要,請為你的test定義要共享的函式。


讀到這裡,你或許會有疑問,準備物件時我是應該用建構函式還是SetUp函式?在釋放資源時,我是應該用解構函式,還是TearDown函式?

首先你需要了解一個test fixture的執行過程:

  • Step 1. 建立一個全新的test fixture物件(會呼叫建構函式)
  • Step 2. 立即呼叫SetUp()
  • Step 3. 執行test程式
  • Step 4. 呼叫TearDown()
  • Step 5. 立即刪除test fixture物件(會呼叫解構函式)

所以,看上去,你沒有必要寫SetUp()和TearDown()函式,只要在構造和解構函式中寫下你要做的事情就可以了。但事實並非如此,在一些場景下,你就必須使用SetUp()和TearDown()函式,讓我們來看看這些場景:

  • View 1. 如果你要在收尾時丟擲異常,則你必須在TearDown中丟擲,而非解構函式中。這是因為在解構函式中丟擲異常,會引發不可預期的結果。
  • View 2. 因為assertion自身在失敗時就會丟擲異常,所以在收尾時,如果你仍想呼叫assertion,則也只能在TearDown()中,而非解構函式中。
  • View 3. 在構造和解構函式中,不能呼叫虛擬函式,所以,如果你想呼叫一個過載過的虛擬函式,則只能在SetUp()和TearDown()中。

綜上,建議大家把初始化的邏輯都寫到SetUp()中,而將收尾邏輯都寫到TearDown()中。

當你使用了test fixture,請使用TEST_F()巨集來代替TEST()巨集,語法格式如下:

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

和TEST()類似,TEST_F()的第一個引數也是所屬test case的名稱,對於TEST_F來說,還要保證這個test case名稱同時也是test fixture類的名稱。(或許你已經猜到了,TEST_F的_F就是指fixture)。

在定義TEST_F()之前,你就應該先定義好test fixture類,否則編譯器會報錯“virtual outside class declaration”。

我們用TEST_F()來定義test,gtest會按照如下流程來動作:

  • Step 1. 建立一個全新的test fixture物件(會呼叫建構函式)
  • Step 2. 立即呼叫SetUp()
  • Step 3. 執行test程式
  • Step 4. 呼叫TearDown()
  • Step 5. 立即刪除test fixture物件(會呼叫解構函式)

需要注意的是,在同一個test case中的不同test會擁有不同的test fixture物件,並且gtest總是會先刪除一個test fixture後才建立下一個新的test fixture。gtest不會在多個test之間複用同一個test fixture。對一個test fixture所作的改變不會影響其他的test的效果。

我們來看一個例子,我們實現了一個FIFO佇列的模板類,名叫Queue,它的外部介面包括這些:

1
2
3
4
5
6
7
8
9
template // E is the element type.
class Queue {
public:
Queue();
void Enqueue(const E& element); // 元素加入佇列
E* Dequeue(); // 從佇列中清除,若佇列為空則返回NULL
size_t size() const; // 獲取佇列長度
...
};

首先,我們定義一個fixture類:

1
2
3
4
5
6
7
8
9
10
11
12
13
class QueueTest : public ::testing::Test {
protected:
virtual void SetUp() {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
  
// virtual void TearDown(){}
Queue q0_; // 模板類的例項
Queue q1_;
Queue q2_;
};

因為我們沒有分配什麼資源,所以我們也不需要在TearDown()中進行收尾工作。

下面,我們就來使用TEST_F()和這個fixture寫我們的test了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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_*兩類assertion。由於在本文開頭部分已經多次講過兩種assertion的區別,所以,相信大家對於在何種情況下應該使用哪種,應該已經很清楚了。

當上面這個test執行,會發生下面一系列的動作:

  • 1. gtest執行建構函式,建立QueueTest物件(命名為t1)
  • 2. t1.SetUp()執行
  • 3. 第一個test(IsEmptyInitially)執行
  • 4. t1.TearDown()執行
  • 5. t1被析構
  • 6. 1-5的動作在另一個QueueTest物件上被再次執行,以執行DequeueWorks這個test。

【讓test執行起來】

TEST()和TEST_F()都是用於把test註冊到test case中。不像其他一些C++測試框架,大家不需要在觸發執行時再重新列出所有已註冊的test。

如果要觸發執行,請執行RUN_ALL_TESTS()巨集,如果所有的test都測試通過,它會返回0,否則會返回1。

當你呼叫了RUN_ALL_TESTS()巨集時,會依次發生下面這些事情:

  • 1. 儲存gtest所有的狀態資訊
  • 2. 為第一個test建立test fixture物件
  • 3. 通過SetUp()初始化
  • 4. 基於這個fixture物件,執行test
  • 5. 通過TearDown()收尾
  • 6. 刪除fixture物件
  • 7. 恢復gtest的各類狀態資訊
  • 8. 為下一個test,重複1-7步,直到所有test執行完成

如果在第2步中,建構函式產生fatal failure,則3-5步就不會被執行了,而是直接跳到第6步。同樣的,如果第3步產生fatal failure,則第4步不會執行,而是直接跳到TearDown()。

需要大家注意的一個很重要的點是,你不能忽略RUN_ALL_TESTS()的返回值,否則gcc會報出編譯錯誤的。這是因為gtest只會依賴於這個返回值來判斷這次測試是否通過,所以這個值對gtest來說非常重要。因此,你需要在你的main函式中return這個值。

另外,你只能呼叫一次RUN_ALL_TESTS()。如果多次呼叫,會產生一些高階衝突。

【編寫測試的main函式】

你可以參考如下的這個樣板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#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
}
  
virtual void TearDown() {
// Code here will be called immediately after each test
}
  
// Object 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) {
// Execises the Xyz feature of Foo.
}
} // namespace
  
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

下面我們就來看看上面這段程式碼,其中::testing::InitGoogleTest函式會解析命令列輸入的引數,這允許我們可以通過命令列引數來控制測試程式的一些行為,具體用法可以參考“高階指南”。

而後,就可以在return 中呼叫RUN_ALL_TESTS()了,這也保證了測試結果可以通過main函式返回。

相關文章