C++單元測試!寫的很好!!轉 C++單元測試的一點感悟

C++單元測試的一點感悟

      之前一直在尋找一種合適的方法來做C++單元測試,也嘗試了不少的方法。寫一點體會提供大家參考(不一定是最好的,但是我想還是能給大家一些啓發吧)。JAVA和C#都有強大的IDE支持,而且JAVA和C#的反射機制能夠使得Mock更加容易一些。但是由於C/C++語言的獨特性,單元測試的過程變得不那麼的順手,特別是工作在Linux的程序猿們,可能還在使用最原始的文本編輯器VI+Makefile來編寫自己的代碼。而且在實際的C++的項目中,很多程序員做的所謂的單元測試,其正確的定義應該爲集成測試或者接口測試。因爲,他們針對的測試更多的是從最上層的接口調用來展開的(當然,這裏不是絕對,我只是以我個人的經歷舉例說明)。此篇文章將結合本人多年的C++編程經驗和測試經驗來探討和尋找一種更加合理的C++單元測試的方法,使得C++單元測試更可行一些。

首先,我們分析一下在C++單元測試實踐過程中,可能會面臨如下的非常實際的情況:

1.   一個工程會包含很多類,這些類又相互依賴。這些關係可能是錯綜複雜的,沒有辦法分離。

2.   一個工程可能會調用或者依賴第三方的服務,而第三方的服務在項目初期可能無法使用或者在測試環境中無法使用。

3.   如何組織測試代碼和被測代碼,並且將測試代碼引入到工程,而且測試代碼不能影響開發代碼。

4.   採用何種方式編譯測試代碼,而且測試程序完全獨立於產品程序

本篇文章將圍繞上述4個問題來集中的分析和展開,共同探討更加可行的單元測試方案。此篇文章只是基於本人的個人經歷,也許您會有更合適或者合理的方法,歡迎共同探討。

目前,市面上也提供了很多單元測試框架的選擇,如:CppUnit,CppUnitLite,GTest。但是,當你去網上Google或者Baidu,希望能夠得到一些有用的實例時,你可能會發現這些實例都不是你想要的。因爲他們僅僅停留在最基本的幾個案例上,幾乎和hello world差不多。而你的項目可能是很複雜的,有大量的依賴,無法套用這些簡單的實例。本篇文章將結合本人實際的項目經驗來逐一探討以下幾種解決方案,來分析各種解決方案的利弊,尋找更加可行的解決方案。(以下給出的案例分析,將從整體架構設計上給出具體的分析,測試框架以GTest爲例。

產品架構:


                                                     Sample產品架構示意圖

上圖是Sample產品的類結構圖。如果我們需要對Class C做單元測試,那麼Class C依賴Class B,MySQL Utils,ThirdService,並且Class B還繼承於Class A。我們將依次給出3種解決方案來分析各種解決方案的優缺點。

 

方案一:產品代碼分離測試

將Class C的代碼單獨測試並且不包含其他任何產品代碼。由於Class C依賴Class B,MySQLUtils,第三方服務,那麼意味着你的測試代碼需要Mock Class B, Mock MySQLUtils, Mock Third Service。以下是測試程序的結構:



從上圖的結構不難看出,此方案除了測試代碼還需要寫大量的Mock代碼。而且編寫Mock代碼可能是非常耗費時間和經歷的,從某種程度上來說,此方案加大了測試的複雜性和工作量。對於快速迭代的產品,比如互聯網公司的一些特性,需要在一至兩週內迭代發佈產品。這樣的單元測試設計可能不是太實際。但是,此方案最大的優勢也在於Mock的強制性。首先,依賴的服務的Mock率必須是100%,因爲你不把所有依賴的服務都Mock掉,是無法通過編譯的。其次,Mock可以無需藉助於任何的框架,只需將Mock類的定義以及函數定義寫得和被Mock的真實類一模一樣就行了,因爲實際的測試代碼並未將真實的依賴產品代碼一同編譯。而且也必須這樣做,如果不Mock的一模一樣是無法通過編譯的。而且一些私有或者函數內部變量也是可以被Mock的,如下代碼:

string GetToken(stringusername)

{

      ThirdService svc;

      svc.GetToken(username);

      …….

}

用過 gmock的同學可能會了解,如果ThirdService(第三方服務)目前不可用的情況下,使用gmock是沒法將ThirdService Mock掉的。gmock採用的是繼承的方式,而ThirdService在函數內部,將無法將被Mock的ThirdService的實例傳入。而我們討論的方案一,爲了測試該函數,你需要重新實現一個ThirdService,其所有類命名和函數類名都必須和真實的ThirdService一模一樣,實現可能不一樣,根據測試要求安排。那麼,方案一使得GetToken函數可測(在“單元測試設計”文章中會集中討論如何編寫你代碼使得代碼可測)

方案一的特點是:

   1.   依賴全量Mock,測試成本高,難度較大

   2.   依賴服務相對較小的項目

   3.   測試覆蓋面廣,代碼可測性強

   4.   適合代碼質量要求高,並且開發時間充裕的項目

 

方案二:測試代碼和產品代碼一體化

方案一在複雜和多依賴的項目中,需要大量的Mock工作,增加了測試的複雜度。如果我們將測試代碼和產品的整個工程編譯在一起,這樣測試代碼就可以調用到產品代碼幾乎所有資源,解決掉依賴的問題。



從方案二可以看出,大量的Mock已經消失。測試代碼和產品所有的代碼編譯在一起,最大的好處就是能夠拿到產品幾乎所有的資源(當然不包括哪些private資源或者內部變量等),對public的函數做單元測試已經可以滿足需求了。對所有的函數做單元測試(包括:private函數),我個人覺得只是一種理想的狀態。很多情況由於項目這樣那樣的原因,只能保證部分函數或者核心代碼的單元測試。當然對private的函數也是有方法去做單元測試的,本篇不做討論。

那方案二是完美的嗎?答案:非也。方案二有一個嚴重的問題,以gtest框架爲例,gtest要求main()函數中初始化gtest框架和執行RUN_ALL_TESTS()。從上圖可以看出,產品的代碼Main.cpp已經包含了產品的main()函數。那麼,方案二意味着需要修改產品的代碼將gtest初始化和RUN_ALL_TESTS()加入至產品的main()函數中。可能你首先會想到用宏開關控制,如果當前編譯的是測試代碼,定義一個測試宏,並將gtest的初始化和RUN_ALL_TESTS()編譯。如果當前編譯的是產品代碼,則gtest的初始化和RUN_ALL_TESTS()不進行編譯。這樣確實能夠滿足要求,但是這樣會破壞產品代碼的純潔性。我們的目標是產品代碼不會包含任何的測試代碼,保證產品代碼的純潔性。所以,方案二也不是那樣的完美,那我們能否做到對產品代碼零修改呢?接下來,我們看方案三。

 

方案三:產品main()函數自動剝離

方案二已經減輕了Mock的工作量,但是由於需要修改產品的main()函數,打破了產品代碼的純潔性。在方案三中將利用編譯器的優化功能來解決此問題。首先,我們看一下方案三的架構設計:


方案三首先將產品代碼編譯成一個static庫(sample.a),而非執行程序,然後j將其和測試代碼鏈接成一個測試執行程序。你可能會問這樣就可以解決方案二的問題嗎?細心的讀者可能會發現上圖中有兩個main()函數,在測試代碼TestC.cpp中包含了一個main()函數,該main()函數中添加了gtest初始化和RUN_ALL_TESTS()。在產品代碼Main.cpp中也有一個產品的main()函數。那一個執行程序可以包含兩個main()函數嗎?答案:“當然不行”。這裏利用了編譯器的一點點小技巧,編譯器在鏈接一個靜態庫時,發現靜態庫中的main()函數和目標代碼中的main()函數衝突時,編譯器會自動的將靜態庫中的main()函數剝離掉。最終的執行程序只會保留目標代碼中的main()函數,即TestC.cpp中的main()函數。但是,前提是靜態庫中的Main.cpp沒有函數被別的函數調用。其實編譯器做的事情等同於將Main.o刪除掉了,但是如果Main.o中還有函數被其他函數調用,那麼其他.o文件會依賴於Main.o,此時Main.o是沒有辦法被剝離的。方案三適用於main()函數放在一個單獨的cpp中的情況。

方案三的特點:

1.    消除了測試的高Mock性,一定程度上減輕了測試負擔

2.    測試代碼完全獨立於開發代碼,保持了開發代碼的純潔性,完全通過makefile控制

3.    要求產品代碼的main()函數獨自存在一個cpp中

 

綜上所述,其實每種解決方案都有自己的優劣勢,開發人員需要根據自己的項目情況來選擇合適的解決方案。

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