適合於跨平臺的C++測試工具
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函式返回。
相關文章
- 基於 Django 的 Dubbo 介面測試工具平臺Django
- 基於多語言的跨平臺靜態測試解決方案
- 關於測試平臺的搭建 (我們要不要搭建測試平臺)
- 如何選擇合適的自動化測試工具?
- 使用c++開發跨平臺的程式C++
- 如何選擇合適的移動應用測試工具?
- 對比四款專業的測試工具,幫助你選出最適合自己的測試工具
- 跨平臺同步筆記工具筆記
- 短視訊的平臺有哪些?哪些平臺適合新手?
- C++跨平臺庫boost和Poco的編譯C++編譯
- 工作流-跨平臺的排程工具
- 對比測試工具平臺讓財務測試飛起來
- 利用 lenosp 腳手架搭建測試工具平臺
- 正確選擇合適的移動應用測試工具很重要
- 測試平臺之介面測試
- 跨平臺ssh客戶端工具Termius客戶端
- 開發適用於微信小程式的跨平臺圖表庫:part1微信小程式
- Azure data studio 跨平臺資料庫管理工具試用資料庫
- 測試平臺起航
- 基於 RF 的 WEB 版自動管理測試平臺Web
- 短影片主要平臺有哪些?教你怎麼選擇合適的平臺!
- 如何選擇一款適合自己的APP自動化測試工具?APP
- 跨平臺的C/C++整合開發環境 CLion 2022C++開發環境
- 新潮測試平臺之效能測試
- 如何在網路上找到適合的外鏈平臺
- 有哪些適合中小企業使用的PaaS平臺?
- Frida-跨平臺注入工具基礎篇
- 聊聊效能測試平臺
- PR效能測試工具升級到全鏈路效能測試與分析平臺
- RestCloud測試平臺,支援壓力測試RESTCloud
- JAVA的跨平臺原理Java
- PR效能測試軟體適用於哪些測試
- 適合中小企業的低程式碼平臺有哪些?
- Flash Party的亂鬥玩法適合手遊平臺嗎?
- rust跨平臺Rust
- 低程式碼平臺適合哪些人群使用?
- 外鏈建設平臺:選擇合適平臺,快速建設外鏈
- 中小團隊選擇一款合適的測試用例管理工具
- 跨瀏覽器測試需要面臨哪些挑戰?跨瀏覽器測試工具分享瀏覽器