使用gtest也有很長(zhǎng)一段時(shí)間了,這期間也積累了一些經(jīng)驗(yàn),所以分享一下。GTest為我們提供了便捷的
測(cè)試框架,讓我們只需要關(guān)注案例本身。如何在GTest框架下寫出優(yōu)美的
測(cè)試案例,我覺得必須要做到:
案例的層次結(jié)構(gòu)一定要清晰
案例的檢查點(diǎn)一定要明確
案例失敗時(shí)一定要能精確的定位問題
案例執(zhí)行結(jié)果一定要穩(wěn)定
案例執(zhí)行的時(shí)間一定不能太長(zhǎng)
案例一定不能對(duì)測(cè)試環(huán)境造成破壞
案例一定獨(dú)立,不能與其他案例有先后關(guān)系的依賴
案例的命名一定清晰,容易理解
案例的可維護(hù)性也是非常重要,如果做到上面的8點(diǎn),自然也就做到了可維護(hù)性。下面來分享一下我對(duì)于上面8點(diǎn)的經(jīng)驗(yàn):
1. 案例的層次結(jié)構(gòu)一定要清晰
所謂層次結(jié)構(gòu),至少要讓人一眼就能分辨出被測(cè)代碼和測(cè)試代碼。簡(jiǎn)單的說,就是知道你在測(cè)什么。由于是進(jìn)行
接口測(cè)試,我已經(jīng)習(xí)慣了如下的案例層次:
DataDefine
我會(huì)將測(cè)試案例所需要的數(shù)據(jù),以及數(shù)據(jù)之間的聯(lián)系全部在預(yù)先定義好。測(cè)試數(shù)據(jù)與案例邏輯的分離,有利于維護(hù)和擴(kuò)展測(cè)試案例。同時(shí),GTest先天就支持測(cè)試數(shù)據(jù)參數(shù)化,為測(cè)試數(shù)據(jù)的分離提供了進(jìn)一步的便捷。什么是測(cè)試數(shù)據(jù)參數(shù)化?就是你可以預(yù)先定義好一批各種各樣的數(shù)據(jù),而你只需要編寫一個(gè)測(cè)試案例的邏輯代碼,gtest會(huì)將定義好的數(shù)據(jù)逐個(gè)套入測(cè)試案例中進(jìn)行執(zhí)行。具體的做法請(qǐng)見:玩轉(zhuǎn)
Google開源C++單元測(cè)試框架Google
Test系列(gtest)之四 - 參數(shù)化
SUT
SUT,即system under test,表明你的測(cè)試對(duì)象是什么,它可以是一個(gè)類(CUT),對(duì)象(OUT),函數(shù)(MUT),甚至可以是整個(gè)應(yīng)用程序(AUT)。我單獨(dú)將這個(gè)層次劃分出來,主要有兩個(gè)目的:
(1)明確的表示出你的測(cè)試對(duì)象是什么
(2)為復(fù)雜調(diào)用對(duì)象包裝簡(jiǎn)單調(diào)用接口
明確表示測(cè)試對(duì)象是什么,便于之后對(duì)測(cè)試案例的維護(hù)和對(duì)測(cè)試案例的理解。同時(shí),對(duì)于一些被測(cè)對(duì)象,你想要調(diào)用它需要經(jīng)過一系列煩瑣的過程,這時(shí),就需要將這一煩瑣的調(diào)用過程隱藏起來,而只關(guān)注被測(cè)對(duì)象的輸入和輸出。
TestCase
測(cè)試工程中,必須非常明確的表示出哪些是測(cè)試案例,哪些是其他的輔助文件。通常,我們會(huì)在測(cè)試案例的文件名加上Test前綴(或者后綴)。我建議,將所有的測(cè)試案例文件或代碼放在最顯眼的地方,讓所有看到你的測(cè)試工程的人,第一眼看到的就是測(cè)試案例,這很重要。
Checker
對(duì)于一個(gè)復(fù)雜系統(tǒng)的接口測(cè)試,僅僅堅(jiān)持輸入和輸出是遠(yuǎn)遠(yuǎn)不夠的。比如測(cè)試一個(gè)寫數(shù)據(jù)庫的函數(shù),函數(shù)的返回值告訴你數(shù)據(jù)已經(jīng)成功寫入是遠(yuǎn)遠(yuǎn)不夠的,你必須親身去數(shù)據(jù)庫中查個(gè)究竟才行。因此,對(duì)于某一類的測(cè)試案例,我們可以抽象出一些通用的檢查點(diǎn)代碼。
如果做到上面的分層,那么一個(gè)測(cè)試案例寫出來的結(jié)構(gòu)應(yīng)該會(huì)是這個(gè)樣子:
TEST(TestFoo, JustDemo) { GetTestData(); // 獲取測(cè)試數(shù)據(jù) CallSUT(); // 調(diào)用被測(cè)方法 CheckSomething(); // 檢查點(diǎn)驗(yàn)證 } |
這樣的測(cè)試案例,一目了然。
2. 案例的檢查點(diǎn)一定要明確
一定要明確案例的檢查點(diǎn)是什么,并且讓檢查點(diǎn)盡量集中。有一個(gè)不好的習(xí)慣就是核心的檢查點(diǎn)在分布在多個(gè)函數(shù)中,需要不斷的跳轉(zhuǎn)才能了解到這個(gè)案例檢查了些什么。好的做法應(yīng)該是盡量讓檢查點(diǎn)集中,能夠非常清晰的分辨出案例對(duì)被測(cè)代碼做了哪些檢查。所以,盡量讓Gtest的ASSERT_和EXPECT_系列的宏放在明顯和正確的地方。
3. 案例失敗時(shí)一定要能精確的定位問題
測(cè)試案例失敗時(shí),我們通常手忙腳亂。如果一個(gè)測(cè)試案例Failed,卻不能立即推斷是被測(cè)代碼的Bug的話,這個(gè)測(cè)試案例也有待改進(jìn)。我們可以在一些復(fù)雜的檢查點(diǎn)斷言中加入一些輔助信息,方便我們定位問題。比如下面這個(gè)測(cè)試案例:
int n = -1; bool actualResult = Foo::Dosometing(n); ASSERT_TRUE(actualResult) |
如果測(cè)試案例失敗了,會(huì)得到下面的信息:
Value of: actualResult Actual: false Expected:true |
這樣的結(jié)果對(duì)于我們來說,幾乎沒有什么用。因?yàn)槲覀兏静恢繿ctualResult是什么,以及在什么情況下才會(huì)出現(xiàn)非預(yù)期值。因此,在斷言處多加入一些信息,將有助于定位問題:
int n = -1; bool actualResult = Foo::Dosometing(n); ASSERT_TRUE(actualResult) << L"Call Foo::Dosometing(n) when n = " << n; |
4. 案例執(zhí)行結(jié)果一定要穩(wěn)定
要保證測(cè)試案例在什么時(shí)候、什么情況下執(zhí)行的結(jié)果都是一樣的。一個(gè)一會(huì)成功一會(huì)失敗的案例是沒有意義的。要保證案例穩(wěn)定性的方法有很多,比如杜絕案例之間的影響,有時(shí)候,由于前一個(gè)案例執(zhí)行完后,將一些系統(tǒng)的環(huán)境破壞了,導(dǎo)致后面的案例執(zhí)行失敗。在測(cè)試某些本身就存在一定幾率或延時(shí)的系統(tǒng)時(shí),使用超時(shí)機(jī)制是比較簡(jiǎn)單的辦法。比如,你需要測(cè)試一個(gè)啟動(dòng)Windows服務(wù)的方法,如果我們?cè)谡{(diào)用了該方法后立即進(jìn)行檢查,很可能檢查點(diǎn)會(huì)失敗,有時(shí)候也許又是通過的。這是因?yàn)閃indows服務(wù)由Stop狀態(tài)到Running狀態(tài),中間還要經(jīng)過一個(gè)Padding狀態(tài)。所以,簡(jiǎn)單的做法是使用超時(shí)機(jī)制,隔斷時(shí)間檢查一次,直到超過某個(gè)最大忍受時(shí)間。
ASSERT_TRUE(StartService('xxx')); int tryTimes = 0; int status = GetServiceStatus('xxx'); while (status != Running) { if (tryTimes >= 10) break; ::Sleep(200); tryTimes++; status = GetServiceStatus('xxx'); } ASSERT_EQ(Running, status) << "Check the status after StartService('xxx')"; |
5. 案例執(zhí)行的時(shí)間一定不能太長(zhǎng)
我們應(yīng)該盡量讓案例能夠快速的執(zhí)行,一方面,我們可以通過優(yōu)化我們的代碼來減少運(yùn)行時(shí)間,比如,減少對(duì)重復(fù)內(nèi)容的讀取。一方面,對(duì)于一些比較耗時(shí)的操作,比如文件系統(tǒng),網(wǎng)絡(luò)操作,我們可以使用Mock對(duì)象來替代真實(shí)的對(duì)象。使用GMock是一個(gè)不錯(cuò)的選擇。
6. 案例一定不能對(duì)測(cè)試環(huán)境造成破壞
有的案例需要在特定的環(huán)境下來能執(zhí)行,因此會(huì)在案例的初始化時(shí)對(duì)環(huán)境進(jìn)行一些修改。注意,不管對(duì)什么東西進(jìn)行了修改,一定要保證在案例執(zhí)行完成的TearDown中將這些環(huán)境都還原回來。否則有可能對(duì)后面的案例造成影響,或者出現(xiàn)一些莫名其妙的錯(cuò)誤。
7. 案例一定獨(dú)立,不能與其他案例有先后關(guān)系的依賴
任何一個(gè)案例都不依賴于其他測(cè)試案例,任何一個(gè)案例的執(zhí)行結(jié)果都不應(yīng)該影響到別的案例。任何一個(gè)案例都可以單獨(dú)拿出去正確的執(zhí)行。所以,不能寄希望于前一個(gè)案例所做的環(huán)境準(zhǔn)備,因?yàn)檫@是不對(duì)的。
8. 案例的命名一定清晰,容易理解
案例的名字要規(guī)范,長(zhǎng)不要緊,一定要清晰的表達(dá)測(cè)試案例的用途。比如,下面的測(cè)試案例名稱都是不好的:
TEST(TestFoo, Test) TEST(TestFoo, Normal) TEST(TestFoo, Alright) |
比如像下面的案例名稱就會(huì)好一點(diǎn):
TEST(TestFoo, Return_True_When_ParameterN_Larger_Then_Zero) TEST(TestFoo, Return_False_When_ParameterN_Is_Zero) |