作者:趙靜 團隊:騰訊移動品質中心TMQ
單元測試(英語:Unit Testing)又稱為子產品測試,是針對程式子產品(軟體設計的最小機關)來進行正确性檢驗的測試工作。程式單元是應用的最小可測試部件。在過程化程式設計中,一個單元就是單個程式、函數、過程等。
對于面向對象程式設計,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法—摘自維基百科。
16年下半年對滴滴SDK接口進行梳理,并進行了BVT接口自動化以及截圖半自動化效果驗證,但是有幾個問題沒能得到很好的解決:
(1)SDK的整體代碼行覆寫率是57.6%,但導航引擎的覆寫率僅31.2%;
(2)從SDK這層測試導航引擎,需要回放不同類型的軌迹,測試效率低;
(3)從端上直接測試引擎,不符合分層測試思想,較難發現深層次問題。
無論是做自動化測試也好,內建測試也罷,都需要對待測子產品有一定程度的了解,對于單元測試這種需要深入代碼邏輯的測試來講,更是如此。在開展測試之前,主要從幾個方面對待測子產品進行分析:代碼邏輯、圈複雜度、代碼深度、扇入、扇出以及代碼行等,如下圖1所示:

圖1可測性分析
可以看到,該子產品有些接口的圈複雜度達到了200+,而業内設計較好的代碼圈複雜度在15左右,對這類接口,不建議做UT,最好的方法是讓開發進行優化,降低函數的圈複雜度。
工欲善其事必先利其器,對UT而言也是如此。C++的曆史已經非常悠久了,開源架構也是非常多,其中google公司出品的gtest和gmock就是做C++單測的必備神器(https://github.com/google/googletest)。
目前該測試架構可以支援Windows、Linux以及Mac OSX平台。
結合SDK實際情況,整合gtest和gmock架構至測試分支,如下圖2所示:
圖2代碼組織結構
這裡的UT是嵌入到開發工程裡的,做為開發源碼WorkSpace中的一個target,該target和之前BVT的target的差別在于,其是基于MAC OSX的Command Line工程,運作環境是MAC OSX,類似于Windows下的可執行檔案,而BVT自動化的case運作環境都是基于iOS或者是iOS Simulator系統,這些差别所帶來的影響會在第4節中詳細說明。
環境部署好了,剩下的就是根據之前的接口分析來設計單測case了。這裡舉一個簡單的例子來進行說明,被測接口是getItem,代碼邏輯比較簡單,如下圖3所示:
圖3被測接口
如何設計case呢?對這種既有入參,又有傳回值的函數,相對是比較好設計case并進行結果驗證的,我們重點關注入參i在不同取值的情況下,函數傳回結果是否符合預期。測試代碼的編寫如下圖4:
圖4測試用例
這樣的case是不是很簡單,但在寫單測的過程中,我們所面對的測試對象往往複雜的超出你的想象。
開發在設計類時,對于不想讓外部類通路的屬性以及方法都可以定義為私有的,這并沒有什麼設計上的問題,但對于測試而言,就要突破這種通路限制,做到public和非public接口都可以在測試類中被通路到,對這個問題,最簡潔快速的方法是:在測試類中将private、protected關鍵字重定義為public,之後在測試類中就可以通路到被測函數的所有方法以及屬性。代碼如下圖5:
圖5private可通路
對于C++中的異步回調,可以采用異步變同步的方法,保證該調的時候可以正常的調用。
1)為什麼無法mock static類型的函數?
在Google Mock的官方“常見問題”的回答中,Google是這樣的:You can, but you need to make some changes.即如果你需要mock一個靜态函數,那說明你的程式子產品過于“緊耦合”了(并且靈活性不夠、重用性不夠、可測試性不夠),你最好是定義一個小接口,通過這個接口來調用那個函數,然後就容易mock了。
2)為什麼無法mock非虛函數?
C++ allows a subclass to change the access level of a virtual function in the base class。C++允許用基類的指針來調用子類的函數,舉個例子,就很容易明白了,如圖6:
圖6基類指針調子類函數
非虛函數不具備這樣的特性,無法很友善的使用gmock。在實際開發過程中,我們不可能将所有的接口都定義為虛函數,那這個問題如何解呢?
方案一
見 google官方手冊https://github.com/google/googletest/blob/master/googlemock/docs/CookBook.md,Google Mock can mock non-virtual functions to be used in what we call hi-perf dependency injection,即依賴注入。該方案的原理是通過模闆類的方式來實作,在開發代碼中通過傳入實際對象來調用真實接口,在測試代碼中通過傳入mock對象來調用mock出來的接口。Google官方提供的一個例子,如圖7:
圖7 依賴注入
方案二
重新定義一個mock類B,該類并不繼承被測類A,但是在mock類B中,需要實作和A中同樣的函數接口,除了待mock的接口。即被測類A和mock類B之間沒有任何關系,mock類B中同樣實作了被測類A中的大部分接口,在測試代碼中,通過聲明mock類B的對象,來達到測試目的。
上述兩種方案都可以解決gmock不能mock非虛函數的問題,但是都并不完美,均有其缺點:方案一最大的問題是需要修改開發源碼,這對于老工程來講,幾乎是不可能的,除非趕上開發重構代碼;方案二雖然不會修改開發源碼,但是需要維護一套開發代碼,當開發代碼有變更時,mock的類B需要進行同步修改,無疑加大了測試的維護成本。
如何解決?——Hook
提到hook,就不得不提百度在11年開源的Baiduhook,其提供了linux平台下C/C++程式的hook功能, 可以解決gmock隻能mock虛函數的限制。Linux上的hook和windows上的原理差不多,操作基本上是對目标函數進行劫持,替換成自己的函數,然後在自己的函數中進行一些使用者預期的操作,比如修改函數傳回值等。對hook原理比較感興趣的可以拜讀下源碼:https://code.google.com/archive/p/baiduhook/
看起來似乎可以解決我們的問題了,但是不幸的是,目前該hook技術僅支援了Linux平台,而我們的測試架構是在MAC OSX系統下搭建的,MAC OSX是Unix系統,bhook無法在MAC下使用。綜合考慮後,決定在Linux系統進行導航引擎的單測。百度以及公司内部都基于hook以及gmock,對gtest進行了二次封裝,形成了自己的單元測試架構btest和ttest。
這兩個測試架構的部署,也是廢了一番周折......這兩個測試架構都依賴Linux的底層系統庫libbfd(二進制檔案描述庫)和libopcodes(程式調試,歸檔等)。
Øttest:須安裝特定版本的binutils以及對應版本的gcc。
1) binutils版本不對
所有的case以及源碼編譯沒有問題,但是在運作case的時候會出現如下圖8所示的core:
圖8binutils版本錯誤引起的core
2)gcc版本不對
gcc5.1版本在編譯gtest源碼庫時,會出現連結錯誤:spec-builders.h:754: undefined reference to `testing::internal::FormatFileLocation
Stack OverFlow上給的解釋是:
Øbtest:仍需要特定版本的Linux系統以及gcc版本。
1)虛拟機centOS4.3+gcc3.4.5
該虛拟機上安裝的btest也隻有相應的lib和so檔案,沒有btest的源碼,直接運作自帶的samples,btest運作完好,沒有相應的core。
注:實際運作過程中對gdb版本也有要求(6.7及以上版本),否則會出現:this=dwarf2_read_address: Corrupted DWARF expression。
2)虛拟機centOS6.5
centOS4.3上整個測試架構運作沒有問題,但是畢竟該版本的系統太老了,centOS官方已經停止維護了,各種軟體包都沒法通過yum來安裝,這也給後續配置vim開發環境帶來了一定程度的麻煩,是以,就想着能否用高版本的centOS來試下btest是否能運作,結果是不行的,同樣會崩到系統庫中。
總結,這兩個測試架構都是基于Linux系統的hook技術,将hook和gmock完美結合,但是都依賴于Linux系統的底層庫,需要特定版本的系統庫。雖然有了btest或者ttest,可以很友善的mock接口,但友善的同時,我們就不會再去思考如何對複雜接口進行解耦和了。
有些曆史接口,其扇出達到了40+,代碼行也有900+,圈複雜度更是達到了400+,對這樣的一類接口,幾乎不具可測性,如果這類接口又是業務中很重要的接口,建議開發一起從可測性角度出發重新設計,達到可測性後再來開展單元測試。
(1)SDK測試的對象是公開的API,這些API有詳細的接口說明文檔。UT的測試對象是内部函數,這些函數沒有任何文檔,需要測試通過debug或者找開發咨詢去了解。
(2)SDK測試可能隻需要了解某個API被設計來幹什麼,對其内部如何設計關心的并不多。UT不單需要知道被測函數的功能是什麼,還要了解其是如何設計的,實作原理是什麼,要求比SDK測試要高。
(3)SDK測試除了要保證接口本身的功能外,更多的還要關心第三方使用者會如何用,即調用場景。UT不需要關心外部如何調,更加聚焦函數本身。
(4)資料構造,UT深入到函數内部,構造的資料不僅僅包含函數入參,還包含函數内部用到的一些資料。
(5)如果代碼發生了重構,UT的曆史case大多數情況下也得跟着重新設計,測試後期的維護成本也很高。
擷取更多測試幹貨,關注微信公衆号:騰訊移動品質中心TMQ
版權所屬,禁止轉載。