1 現有的單元測試框架
單元測試是保證程序正確性的一種有效的測試手段,對於不一樣的開發語言,一般都能找到相應的單元框架。html
藉助於這些單測框架的幫助,可以使得咱們編寫單元測試用例的過程變得便捷而優雅。框架幫咱們提供了case的管理,執行,斷言集,運行參數,全局事件工做,全部的這些使得咱們只需關注:於對於特定的輸入,被測對象的返回是否正常。
那麼,這些xUnit系列的單元測試框架是如何作到這些的了?分析這些框架,發現全部的單元測試框架都是基於如下的一種體系結構設計的。
java
如上圖所示,單測框架中一般包括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/node
Java: EasyMock http://easymock.org/web
一般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。
app
如何對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/(需×××)
【本文首發於:百度測試技術空間】http://hi.baidu.com/baiduqa/blog/item/44a4a6f613b8d7f67609d753.html