Mock方法介紹

遙望星空發表於2017-03-17

1.現有的單元測試框架
單元測試是保證程式正確性的一種有效的測試手段,對於不同的開發語言,通常都能找到相應的單元框架。

藉助於這些單測框架的幫助,能夠使得我們編寫單元測試用例的過程變得便捷而優雅。框架幫我們提供了case的管理,執行,斷言集,執行引數,全域性事件工作,所有的這些使得我們只需關注:於對於特定的輸入,被測物件的返回是否正常。
那麼,這些xUnit系列的單元測試框架是如何做到這些的了?分析這些框架,發現所有的單元測試框架都是基於以下的一種體系結構設計的。

如上圖所示,單測框架中通常包括TestRunner, Test, TestResult, TestCase, TestSuite, TestFixture六個元件。
TestRuner:負責驅動單元測試用例的執行,彙報測試執行的結果,從而簡化測試
TestFixture:以測試套件的形式提供setUp()和tearDown()方法,保證兩個test case之間的執行是相互獨立,互不影響的。
TestResult:這個元件用於收集每個test case的執行結果
Test:作為TestSuite和TestCase的父類暴露run()方法為TestRunner呼叫
TestCase:暴露給使用者的一個類,使用者通過繼承TestCase,編寫自己的測試用例邏輯
TestSuite:提供suite功能管理testCase
正因為相似的體系結構,所以大多數單元測試框架都提供了類似的功能和使用方法。那麼在單測中引入單元測試框架會帶來什麼好處,在現有單元測試框架下還會存在什麼樣不能解決的問題呢?


2.單元測試框架的優點與一些問題
在單元測試中引入單測框架使得編寫單測用例時,不需要再關注於如何驅動case的執行,如何收集結果,如何管理case集,只需要關注於如何寫好單個測試用例即可;同時,在一些測試框架中通過提供豐富的斷言集,公用方法,以及執行引數使得編寫單個testcase的過程得到了最大的簡化。
那這其中會存在什麼樣的疑問了?
我在單元測試框架中寫一個TestCase,與我單獨寫一個cpp檔案在main()方法裡寫測試程式碼有什麼本質卻別嗎?用了單元測試框架,並沒有解決我在對複雜系統做單測時遇到的問題。
沒錯,對於單個case這兩者從本質上說是沒有區別的。單元測試框架本身並沒有告訴你如何去寫TestCase,在這一點上他是沒有提供任何幫助的。所以對於一些複雜的場景,只用單元測試框架是有點多少顯得無能為力的。
使用單元測試框架往往適用於以下場景的測試:單個函式,一個class,或者幾個功能相關class的測試,對於純函式測試,介面級別的測試尤其適用,如房貸計算器公式的測試。
但是,對於一些複雜場景:
 被測物件依賴複雜,甚至無法簡單new出這個物件
 對於一些failure場景的測試
 被測物件中涉及多執行緒合作
 被測物件通過訊息與外界互動的場景
 …
單純依賴單測框架是無法實現單元測試的,而從某種意義上來說,這些場景反而是測試中的重點。
以分散式系統的測試為例,class 與 function級別的單元測試對整個系統的幫助不大,當然,這種單元測試對單個程式的質量有幫助;分散式系統測試的要點是測試程式間的互動:一個程式收到客戶請求,該如何處理,然後轉發給其他程式;收到響應之後,又修改並應答客戶;同時分散式系統測試中通常更關注一些異常路徑的測試,這些場景才是測試中的重點,也是難點所在。
Mock方法的引入通常能幫助我們解決以上場景中遇到的難題。


3.Mock的引入帶來了什麼
在維基百科上這樣描述Mock:In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A computer programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior. of a human in vehicle impacts.
Mock通常是指,在測試一個物件A時,我們構造一些假的物件來模擬與A之間的互動,而這些Mock物件的行為是我們事先設定且符合預期。通過這些Mock物件來測試A在正常邏輯,異常邏輯或壓力情況下工作是否正常。
引入Mock最大的優勢在於:Mock的行為固定,它確保當你訪問該Mock的某個方法時總是能夠獲得一個沒有任何邏輯的直接就返回的預期結果。
Mock Object的使用通常會帶來以下一些好處:
 隔絕其他模組出錯引起本模組的測試錯誤。
 隔絕其他模組的開發狀態,只要定義好介面,不用管他們開發有沒有完成。
 一些速度較慢的操作,可以用Mock Object代替,快速返回。
對於分散式系統的測試,使用Mock Object會有另外兩項很重要的收益:
 通過Mock Object可以將一些分散式測試轉化為本地的測試
 將Mock用於壓力測試,可以解決測試叢集無法模擬線上叢集大規模下的壓力


4.Mock的應用場景
在使用Mock的過程中,發現Mock是有一些通用性的,對於一些應用場景,是非常適合使用Mock的:
 真實物件具有不可確定的行為(產生不可預測的結果,如股票的行情)
 真實物件很難被建立(比如具體的web容器)
 真實物件的某些行為很難觸發(比如網路錯誤)
 真實情況令程式的執行速度很慢
 真實物件有使用者介面
 測試需要詢問真實物件它是如何被呼叫的(比如測試可能需要驗證某個回撥函式是否被呼叫了)
 真實物件實際上並不存在(當需要和其他開發小組,或者新的硬體系統打交道的時候,這是一個普遍的問題)
當然,也有一些不得不Mock的場景:
 一些比較難構造的Object:這類Object通常有很多依賴,在單元測試中構造出這樣類通常花費的成本太大。
 執行操作的時間較長Object:有一些Object的操作費時,而被測物件依賴於這一個操作的執行結果,例如大檔案寫操作,資料的更新等等,出於測試的需求,通常將這類操作進行Mock。
 異常邏輯:一些異常的邏輯往往在正常測試中是很難觸發的,通過Mock可以人為的控制觸發異常邏輯。
在一些壓力測試的場景下,也不得不使用Mock,例如在分散式系統測試中,通常需要測試一些單點(如namenode,jobtracker)在壓力場景下的工作是否正常。而通常測試叢集在正常邏輯下無法提供足夠的壓力(主要原因是受限於機器數量),這時候就需要應用Mock去滿足。
在這些場景下,我們應該如何去做Mock的工作了,一些現有的Mock工具可以幫助我們進行Mock工作。


5.Mock工具的介紹
手動的構造 Mock 物件通常帶來額外的編碼量,而且這些為建立 Mock 物件而編寫的程式碼很有可能引入錯誤。目前,有許多開源專案對動態構建 Mock 物件提供了支援,這些專案能夠根據現有的介面或類動態生成,這樣不僅能避免額外的編碼工作,同時也降低了引入錯誤的可能。
C++: GoogleMock http://code.google.com/p/googlemock/

Java: EasyMock http://easymock.org/

通常Mock工具通過簡單的方法對於給定的介面生成 Mock 物件的類庫。它提供對介面的模擬,能夠通過錄制、回放、檢查三步來完成大體的測試過程,可以驗證方法的呼叫種類、次數、順序,可以令 Mock 物件返回指定的值或丟擲指定異常。通過這些Mock工具我們可以方便的構造 Mock 物件從而使單元測試順利進行,能夠應用於更加複雜的測試場景。
以EasyMock為例,通過 EasyMock,我們可以為指定的介面動態的建立 Mock 物件,並利用 Mock 物件來模擬協同模組,從而使單元測試順利進行。這個過程大致可以劃分為以下幾個步驟:
 使用 EasyMock 生成 Mock 物件
 設定 Mock 物件的預期行為和輸出 
 將 Mock 物件切換到 Replay 狀態
 呼叫 Mock 物件方法進行單元測試
 對 Mock 物件的行為進行驗證
EasyMock的使用和原理: http://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/

EasyMock 後臺處理的主要原理是利用 java.lang.reflect.Proxy 為指定的介面建立一個動態代理,這個動態代理,就是我們在編碼中用到的 Mock 物件。EasyMock 還為這個動態代理提供了一個 InvocationHandler 介面的實現,這個實現類的主要功能就是將動態代理的預期行為記錄在某個對映表中和在實際呼叫時從這個對映表中取出預期輸出。
藉助類似於EasyMock這樣工具,大大降低了編寫Mock物件的成本,通常來說Mock工具依賴於單元測試框架,為使用者編寫TestCase提供便利,但是本身依賴於單元測試框架去驅動,管理case,以及收集測試結果。例如EasyMock依賴於JUint,GoogleMock依賴於Gtest。
那麼有了單元測試框架和相應的Mock工具就萬事俱備了,還有什麼樣的問題?正如單元測試框架沒有告訴你如何寫TestCase一樣,Mock工具也沒有告訴你如何去選擇Mock的點。


6.如何選擇恰當的mock點
對於Mock這裡存在兩個誤區,1.是Mock的物件越多越好;2.Mock會引入巨大的工作量,通常得不償失。這都是源於不恰當的Mock點的選取。
這裡說的如何選擇恰當的mock點,是說對於一個被測物件,我們應當在外圍選擇恰當的mock物件,以及需要mock的介面。因為對於任意一個物件,任意一段程式碼邏輯我們都是有辦法進行Mock的,而Mock點選擇直接決定了我們Mock的工作量以及測試效果。從另外一種意義上來說,不恰當Mock選擇反而會對我們的測試產生誤導,從而在後期的整合和系統測試中引入更多的問題。
在mock點的選擇過程中,以下的一些點會是一些不錯的選擇
 網路互動:如果兩個被測模組之間是通過網路進行互動的,那麼對於網路互動進行Mock通常是比較合適的,如RPC
 外部資源:比如檔案系統、資料來源,如果被測物件對此類外部資源依賴性非常強,而其行為的不可預測性很可能導致測試的隨機失敗,此類的外部資源也適合進行Mock。
 UI:因為UI很多時候都是使用者行為觸發事件,系統本身只是對這些觸發事件進行相應,對這類UI做Mock,往往能夠實現很好的收益,很多基於關鍵字驅動的框架都是基於UI進行Mock的
 第三方API:當介面屬於使用者,通過Mock該介面來確定測試使用者與介面的互動。


當然如何做Mock一定是與被系統的特性精密關聯的,一些強制性的約束和規範是不合適的。這裡介紹幾個做的比較好的mock的例子。
1. 防毒軟體更新部署模組的Mock
這個例子源於一款防毒產品的更新部署模組的測試。對於一個防毒軟體客戶端而言,需要通過更新檢查模組與病毒庫Server進行互動,如果發現病毒庫有更新則觸發病毒庫部署模組的最新病毒庫的資料請求和部署工作,要求部署完成後防毒軟體客戶端能夠正常工作。


對於這一場景的測試,當時受限於這樣一個條件,通常的病毒庫server通常最多一天只更新一次病毒庫,也就是說如果使用真實的病毒庫server,那麼針對更新部署模組的測試一天只能被觸發一次。這是測試中所不能容忍的,通過對病毒庫server進行mock可以解決這個問題。
對於這個場景可以採取這樣一種Mock方式:用一個本地資料夾來模擬病毒庫server,選擇更新部署模組與病毒庫server之間互動的兩個函式checkVersion(),reqData()函式進行Mock。
checkVersion()工作原先的工作是檢查病毒庫Server的版本號,以決定是否觸發更新,將其行為Mock為檢查一個本地資料夾中病毒庫的版本號;reqData()原有的行為是從病毒庫Server拖取病毒庫檔案,將其Mock為從本地資料夾中拖取病毒庫檔案。通過這種方式我們用一個本地資料夾Mock病毒庫Server的行為,其帶來的產出是:我們可以隨意的觸發病毒庫更新操作以及各種異常。通過這種方式發現了一個在更新部署過程中,病毒庫Server的病毒庫版本發生改變造成出錯的嚴重bug,這個是在原有一天才觸發一次更新操作的情況下永遠也無法發現的。
2. 分散式系統中對NameNode模組的測試


在測試NameNode模組的過程中存在這樣一個問題,在正常邏輯無壓力條件下NameNode模組都是工作正常的。但是線上叢集在大壓力的情況下,是有可能觸發NameNode的問題的。但是原有的測試方法下,我們是無法對NameNode模擬大壓力的場景的(因為NameNode的壓力主要來源於DateNode數量,而我們測試叢集是遠遠無法達到線上幾千臺機器的規模的),而NameNode單點的效能瓶頸問題恰恰是測試的重點,真實的DataNode是無法滿足測試需求的,我們必須對DataNode進行Mock。
 


如何對DateNode進行Mock了,最直觀的想法是選擇NameNode與DataNode之間的互動介面進行Mock,也就是他們之間的RPC互動,但是由於NameNode與DataNode之間的互動資訊種類很多,所以其實這並不是一種很好的選擇。
換個角度來想,NameNode之上的壓力是源於對HDFS的讀寫操作造成的NameNode上後設資料的維護,也就是說,對於NameNode而言,其實他並不關心資料到底寫到哪裡去了,只關心資料是否讀寫成功。如果是這種場景Mock就可以變的簡單了,我們可以直接將DataNode上對塊的操作進行mock,比如,對一次寫請求,DataNode並不觸發真實的寫操作,而直接返回成功。通過這種方式,DataNode去除了執行功能,只保留了訊息互動功能,間接的實現了我們的測試需求,且工作量比之第一種方案小很多。
3. 開源社群提供的MRUnit測試框架
在原有框架下,對於MapReduce程式的測試通常是無法在本地驗證的,更不用說對MapReduce程式進行單測了。而MRUnit通過一個簡單而優雅的Mock,卻實現了一個基於MapReduce程式的單測框架。

基於MRUINT框架可以將單測寫成如下形式:


在這個框架中定義了MapDriver,ReducerDriver,MapReduceDriver三個有點類似容器的driver,通過driver來驅動map,reduce或者整個mapreduce過程的執行。
如上例,在driver中設定mapper為IdentityMapper,通過withInput方法設定輸入資料,通過withOutput方法設定預期結果,通過runTest方法來觸發執行並進行結果檢測
他的實現原理是將outputCollector做Mock,outputCollectort中的emit方法實現的邏輯是將資料寫到檔案系統中,Mock後是通過另外一個程式去收集資料並儲存在記憶體中,從而實現最終結果的可檢驗(在自己的資料結構中比對結果)。
實現的原理很簡單,這樣做mock就會精巧,只選擇最底層的一些簡單卻又依賴廣泛的點(依賴廣泛指模組間的資料流通常都走這樣的點過)做mock,這樣通常效果很好且簡單
當然這個例子中也有一些缺陷:1.因為在outputcollector層做mock的資料擷取,使得無法過partition的分桶邏輯;2.這個框架是寫記憶體的,無法最終改成壓力效能測試工具。
 

7 附錄
1. EasyMock示例:
2. A Brief History of Mock Objects
http://ecmp.baidu.com/page/site/dmsqa/document-details?nodeRef=workspace://SpacesStore/c4e4bd14-aa79-417b-b18a-6502141bb3be&cursor=0&showFolders=all
3. http://www.mockobjects.com/(FQ)

 

轉自:http://blog.csdn.net/alicehyxx/article/details/50667167

相關文章