1. 程式人生 > >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中

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