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