1 現(xiàn)有的單元測(cè)試框架
單元測(cè)試是保證程序正確性的一種有效的測(cè)試手段,對(duì)于不同的開發(fā)語(yǔ)言,通常都能找到相應(yīng)的單元框架。

借助于這些單測(cè)框架的幫助,能夠使得我們編寫單元測(cè)試用例的過(guò)程變得便捷而優(yōu)雅。框架幫我們提供了case的管理,執(zhí)行,斷言集,運(yùn)行參數(shù),全局事件工作,所有的這些使得我們只需關(guān)注:于對(duì)于特定的輸入,被測(cè)對(duì)象的返回是否正常。
那么,這些xUnit系列的單元測(cè)試框架是如何做到這些的了?分析這些框架,發(fā)現(xiàn)所有的單元測(cè)試框架都是基于以下的一種體系結(jié)構(gòu)設(shè)計(jì)的。

如上圖所示,單測(cè)框架中通常包括TestRunner, Test, TestResult, TestCase, TestSuite, TestFixture六個(gè)組件。
TestRuner:負(fù)責(zé)驅(qū)動(dòng)單元測(cè)試用例的執(zhí)行,匯報(bào)測(cè)試執(zhí)行的結(jié)果,從而簡(jiǎn)化測(cè)試
TestFixture:以測(cè)試套件的形式提供setUp()和tearDown()方法,保證兩個(gè)test case之間的執(zhí)行是相互獨(dú)立,互不影響的。
TestResult:這個(gè)組件用于收集每個(gè)test case的執(zhí)行結(jié)果
Test:作為TestSuite和TestCase的父類暴露run()方法為TestRunner調(diào)用
TestCase:暴露給用戶的一個(gè)類,用戶通過(guò)繼承TestCase,編寫自己的測(cè)試用例邏輯
TestSuite:提供suite功能管理testCase
正因?yàn)橄嗨频捏w系結(jié)構(gòu),所以大多數(shù)單元測(cè)試框架都提供了類似的功能和使用方法。那么在單測(cè)中引入單元測(cè)試框架會(huì)帶來(lái)什么好處,在現(xiàn)有單元測(cè)試框架下還會(huì)存在什么樣不能解決的問(wèn)題呢?
2 單元測(cè)試框架的優(yōu)點(diǎn)與一些問(wèn)題
在單元測(cè)試中引入單測(cè)框架使得編寫單測(cè)用例時(shí),不需要再關(guān)注于如何驅(qū)動(dòng)case的執(zhí)行,如何收集結(jié)果,如何管理case集,只需要關(guān)注于如何寫好單個(gè)測(cè)試用例即可;同時(shí),在一些測(cè)試框架中通過(guò)提供豐富的斷言集,公用方法,以及運(yùn)行參數(shù)使得編寫單個(gè)testcase的過(guò)程得到了最大的簡(jiǎn)化。
那這其中會(huì)存在什么樣的疑問(wèn)了?
我在單元測(cè)試框架中寫一個(gè)TestCase,與我單獨(dú)寫一個(gè)cpp文件在main()方法里寫測(cè)試代碼有什么本質(zhì)卻別嗎?用了單元測(cè)試框架,并沒(méi)有解決我在對(duì)復(fù)雜系統(tǒng)做單測(cè)時(shí)遇到的問(wèn)題。
沒(méi)錯(cuò),對(duì)于單個(gè)case這兩者從本質(zhì)上說(shuō)是沒(méi)有區(qū)別的。單元測(cè)試框架本身并沒(méi)有告訴你如何去寫TestCase,在這一點(diǎn)上他是沒(méi)有提供任何幫助的。所以對(duì)于一些復(fù)雜的場(chǎng)景,只用單元測(cè)試框架是有點(diǎn)多少顯得無(wú)能為力的。
使用單元測(cè)試框架往往適用于以下場(chǎng)景的測(cè)試:?jiǎn)蝹€(gè)函數(shù),一個(gè)class,或者幾個(gè)功能相關(guān)class的測(cè)試,對(duì)于純函數(shù)測(cè)試,接口級(jí)別的測(cè)試尤其適用,如房貸計(jì)算器公式的測(cè)試。
但是,對(duì)于一些復(fù)雜場(chǎng)景:
? 被測(cè)對(duì)象依賴復(fù)雜,甚至無(wú)法簡(jiǎn)單new出這個(gè)對(duì)象
? 對(duì)于一些failure場(chǎng)景的測(cè)試
? 被測(cè)對(duì)象中涉及多線程合作
? 被測(cè)對(duì)象通過(guò)消息與外界交互的場(chǎng)景
? …
單純依賴單測(cè)框架是無(wú)法實(shí)現(xiàn)單元測(cè)試的,而從某種意義上來(lái)說(shuō),這些場(chǎng)景反而是測(cè)試中的重點(diǎn)。
以分布式系統(tǒng)的測(cè)試為例,class 與 function級(jí)別的單元測(cè)試對(duì)整個(gè)系統(tǒng)的幫助不大,當(dāng)然,這種單元測(cè)試對(duì)單個(gè)程序的質(zhì)量有幫助;分布式系統(tǒng)測(cè)試的要點(diǎn)是測(cè)試進(jìn)程間的交互:一個(gè)進(jìn)程收到客戶請(qǐng)求,該如何處理,然后轉(zhuǎn)發(fā)給其他進(jìn)程;收到響應(yīng)之后,又修改并應(yīng)答客戶;同時(shí)分布式系統(tǒng)測(cè)試中通常更關(guān)注一些異常路徑的測(cè)試,這些場(chǎng)景才是測(cè)試中的重點(diǎn),也是難點(diǎn)所在。
Mock方法的引入通常能幫助我們解決以上場(chǎng)景中遇到的難題。
3 Mock的引入帶來(lái)了什么
在維基百科上這樣描述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通常是指,在測(cè)試一個(gè)對(duì)象A時(shí),我們構(gòu)造一些假的對(duì)象來(lái)模擬與A之間的交互,而這些Mock對(duì)象的行為是我們事先設(shè)定且符合預(yù)期。通過(guò)這些Mock對(duì)象來(lái)測(cè)試A在正常邏輯,異常邏輯或壓力情況下工作是否正常。
引入Mock最大的優(yōu)勢(shì)在于:Mock的行為固定,它確保當(dāng)你訪問(wèn)該Mock的某個(gè)方法時(shí)總是能夠獲得一個(gè)沒(méi)有任何邏輯的直接就返回的預(yù)期結(jié)果。
Mock Object的使用通常會(huì)帶來(lái)以下一些好處:
? 隔絕其他模塊出錯(cuò)引起本模塊的測(cè)試錯(cuò)誤。
? 隔絕其他模塊的開發(fā)狀態(tài),只要定義好接口,不用管他們開發(fā)有沒(méi)有完成。
? 一些速度較慢的操作,可以用Mock Object代替,快速返回。
對(duì)于分布式系統(tǒng)的測(cè)試,使用Mock Object會(huì)有另外兩項(xiàng)很重要的收益:
? 通過(guò)Mock Object可以將一些分布式測(cè)試轉(zhuǎn)化為本地的測(cè)試
? 將Mock用于壓力測(cè)試,可以解決測(cè)試集群無(wú)法模擬線上集群大規(guī)模下的壓力
4 Mock的應(yīng)用場(chǎng)景
在使用Mock的過(guò)程中,發(fā)現(xiàn)Mock是有一些通用性的,對(duì)于一些應(yīng)用場(chǎng)景,是非常適合使用Mock的:
? 真實(shí)對(duì)象具有不可確定的行為(產(chǎn)生不可預(yù)測(cè)的結(jié)果,如股票的行情)
? 真實(shí)對(duì)象很難被創(chuàng)建(比如具體的web容器)
? 真實(shí)對(duì)象的某些行為很難觸發(fā)(比如網(wǎng)絡(luò)錯(cuò)誤)
? 真實(shí)情況令程序的運(yùn)行速度很慢
? 真實(shí)對(duì)象有用戶界面
? 測(cè)試需要詢問(wèn)真實(shí)對(duì)象它是如何被調(diào)用的(比如測(cè)試可能需要驗(yàn)證某個(gè)回調(diào)函數(shù)是否被調(diào)用了)
? 真實(shí)對(duì)象實(shí)際上并不存在(當(dāng)需要和其他開發(fā)小組,或者新的硬件系統(tǒng)打交道的時(shí)候,這是一個(gè)普遍的問(wèn)題)
當(dāng)然,也有一些不得不Mock的場(chǎng)景:
? 一些比較難構(gòu)造的Object:這類Object通常有很多依賴,在單元測(cè)試中構(gòu)造出這樣類通常花費(fèi)的成本太大。
? 執(zhí)行操作的時(shí)間較長(zhǎng)Object:有一些Object的操作費(fèi)時(shí),而被測(cè)對(duì)象依賴于這一個(gè)操作的執(zhí)行結(jié)果,例如大文件寫操作,數(shù)據(jù)的更新等等,出于測(cè)試的需求,通常將這類操作進(jìn)行Mock。
? 異常邏輯:一些異常的邏輯往往在正常測(cè)試中是很難觸發(fā)的,通過(guò)Mock可以人為的控制觸發(fā)異常邏輯。
在一些壓力測(cè)試的場(chǎng)景下,也不得不使用Mock,例如在分布式系統(tǒng)測(cè)試中,通常需要測(cè)試一些單點(diǎn)(如namenode,jobtracker)在壓力場(chǎng)景下的工作是否正常。而通常測(cè)試集群在正常邏輯下無(wú)法提供足夠的壓力(主要原因是受限于機(jī)器數(shù)量),這時(shí)候就需要應(yīng)用Mock去滿足。
在這些場(chǎng)景下,我們應(yīng)該如何去做Mock的工作了,一些現(xiàn)有的Mock工具可以幫助我們進(jìn)行Mock工作。
5 Mock工具的介紹
手動(dòng)的構(gòu)造 Mock 對(duì)象通常帶來(lái)額外的編碼量,而且這些為創(chuàng)建 Mock 對(duì)象而編寫的代碼很有可能引入錯(cuò)誤。目前,有許多開源項(xiàng)目對(duì)動(dòng)態(tài)構(gòu)建 Mock 對(duì)象提供了支持,這些項(xiàng)目能夠根據(jù)現(xiàn)有的接口或類動(dòng)態(tài)生成,這樣不僅能避免額外的編碼工作,同時(shí)也降低了引入錯(cuò)誤的可能。
C++: GoogleMock http://code.google.com/p/googlemock/
Java: EasyMock http://easymock.org/
通常Mock工具通過(guò)簡(jiǎn)單的方法對(duì)于給定的接口生成 Mock 對(duì)象的類庫(kù)。它提供對(duì)接口的模擬,能夠通過(guò)錄制、回放、檢查三步來(lái)完成大體的測(cè)試過(guò)程,可以驗(yàn)證方法的調(diào)用種類、次數(shù)、順序,可以令 Mock 對(duì)象返回指定的值或拋出指定異常。通過(guò)這些Mock工具我們可以方便的構(gòu)造 Mock 對(duì)象從而使單元測(cè)試順利進(jìn)行,能夠應(yīng)用于更加復(fù)雜的測(cè)試場(chǎng)景。
以EasyMock為例,通過(guò) EasyMock,我們可以為指定的接口動(dòng)態(tài)的創(chuàng)建 Mock 對(duì)象,并利用 Mock 對(duì)象來(lái)模擬協(xié)同模塊,從而使單元測(cè)試順利進(jìn)行。這個(gè)過(guò)程大致可以劃分為以下幾個(gè)步驟:
? 使用 EasyMock 生成 Mock 對(duì)象
? 設(shè)定 Mock 對(duì)象的預(yù)期行為和輸出
? 將 Mock 對(duì)象切換到 Replay 狀態(tài)
? 調(diào)用 Mock 對(duì)象方法進(jìn)行單元測(cè)試
? 對(duì) Mock 對(duì)象的行為進(jìn)行驗(yàn)證
EasyMock的使用和原理: http://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/
EasyMock 后臺(tái)處理的主要原理是利用 java.lang.reflect.Proxy 為指定的接口創(chuàng)建一個(gè)動(dòng)態(tài)代理,這個(gè)動(dòng)態(tài)代理,就是我們?cè)诰幋a中用到的 Mock 對(duì)象。EasyMock 還為這個(gè)動(dòng)態(tài)代理提供了一個(gè) InvocationHandler 接口的實(shí)現(xiàn),這個(gè)實(shí)現(xiàn)類的主要功能就是將動(dòng)態(tài)代理的預(yù)期行為記錄在某個(gè)映射表中和在實(shí)際調(diào)用時(shí)從這個(gè)映射表中取出預(yù)期輸出。
借助類似于EasyMock這樣工具,大大降低了編寫Mock對(duì)象的成本,通常來(lái)說(shuō)Mock工具依賴于單元測(cè)試框架,為用戶編寫TestCase提供便利,但是本身依賴于單元測(cè)試框架去驅(qū)動(dòng),管理case,以及收集測(cè)試結(jié)果。例如EasyMock依賴于JUint,GoogleMock依賴于Gtest。
那么有了單元測(cè)試框架和相應(yīng)的Mock工具就萬(wàn)事俱備了,還有什么樣的問(wèn)題?正如單元測(cè)試框架沒(méi)有告訴你如何寫TestCase一樣,Mock工具也沒(méi)有告訴你如何去選擇Mock的點(diǎn)。
6 如何選擇恰當(dāng)?shù)膍ock點(diǎn)
對(duì)于Mock這里存在兩個(gè)誤區(qū),1.是Mock的對(duì)象越多越好;2.Mock會(huì)引入巨大的工作量,通常得不償失。這都是源于不恰當(dāng)?shù)腗ock點(diǎn)的選取。
這里說(shuō)的如何選擇恰當(dāng)?shù)膍ock點(diǎn),是說(shuō)對(duì)于一個(gè)被測(cè)對(duì)象,我們應(yīng)當(dāng)在外圍選擇恰當(dāng)?shù)膍ock對(duì)象,以及需要mock的接口。因?yàn)閷?duì)于任意一個(gè)對(duì)象,任意一段代碼邏輯我們都是有辦法進(jìn)行Mock的,而Mock點(diǎn)選擇直接決定了我們Mock的工作量以及測(cè)試效果。從另外一種意義上來(lái)說(shuō),不恰當(dāng)Mock選擇反而會(huì)對(duì)我們的測(cè)試產(chǎn)生誤導(dǎo),從而在后期的集成和系統(tǒng)測(cè)試中引入更多的問(wèn)題。
在mock點(diǎn)的選擇過(guò)程中,以下的一些點(diǎn)會(huì)是一些不錯(cuò)的選擇
? 網(wǎng)絡(luò)交互:如果兩個(gè)被測(cè)模塊之間是通過(guò)網(wǎng)絡(luò)進(jìn)行交互的,那么對(duì)于網(wǎng)絡(luò)交互進(jìn)行Mock通常是比較合適的,如RPC
? 外部資源:比如文件系統(tǒng)、數(shù)據(jù)源,如果被測(cè)對(duì)象對(duì)此類外部資源依賴性非常強(qiáng),而其行為的不可預(yù)測(cè)性很可能導(dǎo)致測(cè)試的隨機(jī)失敗,此類的外部資源也適合進(jìn)行Mock。
? UI:因?yàn)閁I很多時(shí)候都是用戶行為觸發(fā)事件,系統(tǒng)本身只是對(duì)這些觸發(fā)事件進(jìn)行相應(yīng),對(duì)這類UI做Mock,往往能夠?qū)崿F(xiàn)很好的收益,很多基于關(guān)鍵字驅(qū)動(dòng)的框架都是基于UI進(jìn)行Mock的
? 第三方API:當(dāng)接口屬于使用者,通過(guò)Mock該接口來(lái)確定測(cè)試使用者與接口的交互。
當(dāng)然如何做Mock一定是與被系統(tǒng)的特性精密關(guān)聯(lián)的,一些強(qiáng)制性的約束和規(guī)范是不合適的。這里介紹幾個(gè)做的比較好的mock的例子。
1. 殺毒軟件更新部署模塊的Mock
這個(gè)例子源于一款殺毒產(chǎn)品的更新部署模塊的測(cè)試。對(duì)于一個(gè)殺毒軟件客戶端而言,需要通過(guò)更新檢查模塊與病毒庫(kù)Server進(jìn)行交互,如果發(fā)現(xiàn)病毒庫(kù)有更新則觸發(fā)病毒庫(kù)部署模塊的最新病毒庫(kù)的數(shù)據(jù)請(qǐng)求和部署工作,要求部署完成后殺毒軟件客戶端能夠正常工作。

對(duì)于這一場(chǎng)景的測(cè)試,當(dāng)時(shí)受限于這樣一個(gè)條件,通常的病毒庫(kù)server通常最多一天只更新一次病毒庫(kù),也就是說(shuō)如果使用真實(shí)的病毒庫(kù)server,那么針對(duì)更新部署模塊的測(cè)試一天只能被觸發(fā)一次。這是測(cè)試中所不能容忍的,通過(guò)對(duì)病毒庫(kù)server進(jìn)行mock可以解決這個(gè)問(wèn)題。
對(duì)于這個(gè)場(chǎng)景可以采取這樣一種Mock方式:用一個(gè)本地文件夾來(lái)模擬病毒庫(kù)server,選擇更新部署模塊與病毒庫(kù)server之間交互的兩個(gè)函數(shù)checkVersion(),reqData()函數(shù)進(jìn)行Mock。
checkVersion()工作原先的工作是檢查病毒庫(kù)Server的版本號(hào),以決定是否觸發(fā)更新,將其行為Mock為檢查一個(gè)本地文件夾中病毒庫(kù)的版本號(hào);reqData()原有的行為是從病毒庫(kù)Server拖取病毒庫(kù)文件,將其Mock為從本地文件夾中拖取病毒庫(kù)文件。通過(guò)這種方式我們用一個(gè)本地文件夾Mock病毒庫(kù)Server的行為,其帶來(lái)的產(chǎn)出是:我們可以隨意的觸發(fā)病毒庫(kù)更新操作以及各種異常。通過(guò)這種方式發(fā)現(xiàn)了一個(gè)在更新部署過(guò)程中,病毒庫(kù)Server的病毒庫(kù)版本發(fā)生改變?cè)斐沙鲥e(cuò)的嚴(yán)重bug,這個(gè)是在原有一天才觸發(fā)一次更新操作的情況下永遠(yuǎn)也無(wú)法發(fā)現(xiàn)的。
2. 分布式系統(tǒng)中對(duì)NameNode模塊的測(cè)試

在測(cè)試NameNode模塊的過(guò)程中存在這樣一個(gè)問(wèn)題,在正常邏輯無(wú)壓力條件下NameNode模塊都是工作正常的。但是線上集群在大壓力的情況下,是有可能觸發(fā)NameNode的問(wèn)題的。但是原有的測(cè)試方法下,我們是無(wú)法對(duì)NameNode模擬大壓力的場(chǎng)景的(因?yàn)镹ameNode的壓力主要來(lái)源于DateNode數(shù)量,而我們測(cè)試集群是遠(yuǎn)遠(yuǎn)無(wú)法達(dá)到線上幾千臺(tái)機(jī)器的規(guī)模的),而NameNode單點(diǎn)的性能瓶頸問(wèn)題恰恰是測(cè)試的重點(diǎn),真實(shí)的DataNode是無(wú)法滿足測(cè)試需求的,我們必須對(duì)DataNode進(jìn)行Mock。

如何對(duì)DateNode進(jìn)行Mock了,最直觀的想法是選擇NameNode與DataNode之間的交互接口進(jìn)行Mock,也就是他們之間的RPC交互,但是由于NameNode與DataNode之間的交互信息種類很多,所以其實(shí)這并不是一種很好的選擇。
換個(gè)角度來(lái)想,NameNode之上的壓力是源于對(duì)HDFS的讀寫操作造成的NameNode上元數(shù)據(jù)的維護(hù),也就是說(shuō),對(duì)于NameNode而言,其實(shí)他并不關(guān)心數(shù)據(jù)到底寫到哪里去了,只關(guān)心數(shù)據(jù)是否讀寫成功。如果是這種場(chǎng)景Mock就可以變的簡(jiǎn)單了,我們可以直接將DataNode上對(duì)塊的操作進(jìn)行mock,比如,對(duì)一次寫請(qǐng)求,DataNode并不觸發(fā)真實(shí)的寫操作,而直接返回成功。通過(guò)這種方式,DataNode去除了執(zhí)行功能,只保留了消息交互功能,間接的實(shí)現(xiàn)了我們的測(cè)試需求,且工作量比之第一種方案小很多。
3. 開源社區(qū)提供的MRUnit測(cè)試框架
在原有框架下,對(duì)于MapReduce程序的測(cè)試通常是無(wú)法在本地驗(yàn)證的,更不用說(shuō)對(duì)MapReduce程序進(jìn)行單測(cè)了。而MRUnit通過(guò)一個(gè)簡(jiǎn)單而優(yōu)雅的Mock,卻實(shí)現(xiàn)了一個(gè)基于MapReduce程序的單測(cè)框架。
基于MRUINT框架可以將單測(cè)寫成如下形式:

在這個(gè)框架中定義了MapDriver,ReducerDriver,MapReduceDriver三個(gè)有點(diǎn)類似容器的driver,通過(guò)driver來(lái)驅(qū)動(dòng)map,reduce或者整個(gè)mapreduce過(guò)程的執(zhí)行。
如上例,在driver中設(shè)定mapper為IdentityMapper,通過(guò)withInput方法設(shè)定輸入數(shù)據(jù),通過(guò)withOutput方法設(shè)定預(yù)期結(jié)果,通過(guò)runTest方法來(lái)觸發(fā)執(zhí)行并進(jìn)行結(jié)果檢測(cè)
他的實(shí)現(xiàn)原理是將outputCollector做Mock,outputCollectort中的emit方法實(shí)現(xiàn)的邏輯是將數(shù)據(jù)寫到文件系統(tǒng)中,Mock后是通過(guò)另外一個(gè)進(jìn)程去收集數(shù)據(jù)并保存在內(nèi)存中,從而實(shí)現(xiàn)最終結(jié)果的可檢驗(yàn)(在自己的數(shù)據(jù)結(jié)構(gòu)中比對(duì)結(jié)果)。
實(shí)現(xiàn)的原理很簡(jiǎn)單,這樣做mock就會(huì)精巧,只選擇最底層的一些簡(jiǎn)單卻又依賴廣泛的點(diǎn)(依賴廣泛指模塊間的數(shù)據(jù)流通常都走這樣的點(diǎn)過(guò))做mock,這樣通常效果很好且簡(jiǎn)單
當(dāng)然這個(gè)例子中也有一些缺陷:1.因?yàn)樵趏utputcollector層做mock的數(shù)據(jù)截取,使得無(wú)法過(guò)partition的分桶邏輯;2.這個(gè)框架是寫內(nèi)存的,無(wú)法最終改成壓力性能測(cè)試工具。
7 附錄
1. EasyMock示例:
