什么是單元測試
單元測試是軟件開發(fā)過程中的一種質(zhì)量保證手段。最初的來源是想模仿對硬件芯片做單元測試那樣,在軟件中也能對小的軟件單元進(jìn)行測試,從而保證軟件中某個局部設(shè)計的正確性。
傳統(tǒng)的單元測試定義
傳統(tǒng)軟件單元測試將被測單元的粒度規(guī)定為軟件中最小的功能模塊暂幼。對于C語言通常指一個函數(shù),對于Java或者C++語言通常指一個類移迫。
傳統(tǒng)做法是針對被測單元的實現(xiàn)細(xì)節(jié)進(jìn)行各種白盒測試旺嬉,即針對被測代碼的實現(xiàn)邏輯進(jìn)行各種分支測試和覆蓋測試。
傳統(tǒng)的單元測試由于缺乏自動化工具的支持厨埋,往往在測試中通過打印輸出測試結(jié)果邪媳,由人工比對每次測試是否成功。
現(xiàn)代單元測試定義
隨著技術(shù)的進(jìn)步和人們對軟件單元測試方法的發(fā)展荡陷,現(xiàn)代單元測試的定義已經(jīng)發(fā)生了很大的變化雨效。
單元測試的粒度以軟件設(shè)計的松耦合邊界為粒度,不一定非要局限于函數(shù)和類這么小的粒度废赞。例如對C++的類只用對public的接口進(jìn)行測試徽龟,private接口不測試。對于C語言可以只測試每個文件對外提供服務(wù)的接口唉地,文件內(nèi)的私有輔助函數(shù)可以不測試据悔。關(guān)于測哪些不測試哪些传透,最終遵循的原則是在降低單元測試成本的情況下讓收益最大化。
單元測試最好是針對被測單元的黑盒測試极颓,這樣降低由于被測代碼實現(xiàn)細(xì)節(jié)的改動朱盐,導(dǎo)致單元測試也聯(lián)動修改的頻率。
借助現(xiàn)代化單元測試框架的幫助菠隆,單元測試可做到一鍵式的可反復(fù)的自動化運行兵琳。用例執(zhí)行結(jié)果的成功或失敗完全由計算機來進(jìn)行判斷,無需人工參與骇径。由于借助現(xiàn)代化單元測試框架躯肌,因此用例的編寫需要遵循測試框架的要求。
總結(jié)一下:我們認(rèn)為現(xiàn)代化的單元測試的定義應(yīng)該是:一種滿足一鍵式全自動化運行的軟件單元級別的黑盒測試破衔。
單元測試的價值
我們認(rèn)為遵循現(xiàn)代單元測試最佳實踐的單元測試過程羡榴,可以為軟件團隊帶來如下價值:
單元測試可以讓軟件故障盡早地被發(fā)現(xiàn)。按照統(tǒng)計运敢,軟件故障發(fā)現(xiàn)越晚,成本呈指數(shù)趨勢上升忠售。良好的單元測試讓故障第一時間被發(fā)現(xiàn)传惠,避免故障遺留到后期由于定位修復(fù)難度帶來的更大損失。
單元測試的可回歸性稻扬,為軟件提供了一層安全防護網(wǎng)卦方。這層安全防護網(wǎng)為軟件后續(xù)的重構(gòu)和修改提供了安全保障。
單元測試為軟件單元如何被使用泰佳,天然提供了一份代碼樣例式的使用手冊文檔盼砍。
如果能以測試驅(qū)動開發(fā)(Test Driven Development,簡稱TDD)的方式進(jìn)行單元測試逝她,那么可以把單元測試變成一種設(shè)計行為浇坐,可以驅(qū)動出更松耦合的代碼設(shè)計和實現(xiàn)。
單元測試要求
我們認(rèn)為合格的單元測試應(yīng)該滿足以下要求:
- 測試用例要能夠一鍵式的自動化運行和自動化的結(jié)果判斷黔宛;
- 測試用例之間不能相互依賴和干擾近刘,也就是說每個用例可以獨立地運行;
- 測試用例是可重現(xiàn)的臀晃,也就是說在被測代碼不變的情況下觉渴,測試用例的執(zhí)行結(jié)果應(yīng)該是一致的。測試不應(yīng)該依賴不穩(wěn)定的因素:例如定時器徽惋、線程調(diào)度等等案淋;
- 測試用例應(yīng)該是簡單易于理解的,測試用例要追求可讀性险绘,這樣才能把測試用例同時作為一份接口使用文檔踢京;
單元測試工具
隨著技術(shù)的成熟誉碴,單元測試工具現(xiàn)在已經(jīng)變得很容易獲得和使用了。自從Kent Beck(敏捷軟件開發(fā)方法泰斗漱挚,極限編程和測試驅(qū)動開發(fā)的提出者)為Java語言開發(fā)并開源了JUnit框架后翔烁,一下子將單元測試帶到了一個新的境地。隨后其它語言紛紛效仿JUnit推出了自己的開源單元測試框架旨涝。人們后來對所有編程語言的這一系列框架起了個統(tǒng)一的名稱蹬屹,叫做xUnit測試框架
。
評判xUnit測試框架的標(biāo)準(zhǔn)
目前對于任一編程語言白华,都能找到好幾款開源的的xUnit測試框架慨默,那么如何對比并選擇合適好用的xUnit框架呢?一般從如下幾個維度去評估弧腥。
- 支持自動檢測注冊用例:框架能否支持簡單地構(gòu)造用例并自動注冊測試用例到測試框架中厦取;
- 支持測試Fixture:即是否支持為一組測試用例建立統(tǒng)一的腳手架,方便測試用例的上下文構(gòu)造管搪;
- 強大的斷言系統(tǒng):是否提供強大的斷言系統(tǒng)虾攻,供使用者在用例中描述期望;
- 靈活的Test Suite定義:可以支持靈活的對測試用例分組更鲁;
- 測試能力:是否支持異常測試以及參數(shù)測試霎箍;
- 測試filter定義:可以支持靈活的命令行參數(shù),對運行用例進(jìn)行分組和過濾澡为;
- 測試結(jié)果及報表生成:是否可以生成易于閱讀的測試結(jié)果報告以及報表文件漂坏;
- 用例依賴管理:是否支持編輯用例的依賴關(guān)系,讓用例之間互相組合媒至,但是又不破壞每個用例的獨立性顶别;
- 沙盒模式:是否支持測試用例的沙盒模式,降低每個測試用例上下文清理的工作拒啰;
- 是否開源驯绎,包括公開的文檔和社區(qū)的支持是否全面;
主流C/C++ xUnit測試框架對比
根據(jù)上面提到的判斷維度图呢,我們分析對比一下當(dāng)前主流的C/C++ xUnit測試框架条篷。
測試框架特性 | Boost Test | CppUnit | Gtest | TestNgpp |
---|---|---|---|---|
是否開源 | 是 | 是 | 是 | 是 |
自動檢測注冊 | 良 | 差 | 優(yōu) | 優(yōu) |
斷言能力 | 良 | 較弱 | 優(yōu) | 優(yōu) |
支持Fixture | 支持 | 支持 | 支持 | 支持 |
支持Suite分組 | 支持 | 支持 | 支持 | 支持 |
支持用例過濾 | 支持 | 支持 | 支持 | 支持 |
測試報表 | 不支持 | 支持 | 支持 | 支持 |
測試能力 | 良 | 良 | 優(yōu) | 優(yōu) |
用例依賴管理 | 不支持 | 不支持 | 不支持 | 支持 |
沙盒模式 | 不支持 | 不支持 | 不支持 | 支持 |
社區(qū)使用程度 | 低 | 一般 | 使用程度很高 | 一般 |
通過上面的分析可以看到,主流的C++ xUnit測試框架都是開源的蛤织。其中TestNgpp功能雖然最強大赴叹,但是用戶較少。Google推出的Gtest框架使用范圍最廣指蚜,社區(qū)支持程度也最好乞巧,從功能上來說簡單易用,作為上手框架最為合適摊鸡。其它框架由于各種缺陷不建議再選用了绽媒。
Mock框架推薦
在做單元測試的時候避免不了要為被測代碼打樁蚕冬,而mock框架主要是為了簡化打樁過程。使用mock框架可以讓打樁代碼非常容易撰寫是辕,而且不會侵入實現(xiàn)代碼囤热。比如兩個測試用例需要同一個樁函數(shù):函數(shù)聲明相同但是返回值不同。在沒有mock框架的情況下解決這類問題非常麻煩获三,而mock框架則可以輕而易舉的應(yīng)對此類問題旁蔼。
Mock框架除了提供打樁的功能外,還提供其它更加強大的功能疙教。例如何以監(jiān)聽用戶對打樁代碼的調(diào)用行為棺聊,并監(jiān)控這些行為是否符合預(yù)期。
對于Java語言來說贞谓,可用的mock框架五花八門限佩,選擇范圍非常廣。但是對于C++語言來說裸弦,只有兩款易用的mock框架:gmock和mockcpp祟同。這兩款都是開源軟件,經(jīng)過使用對比理疙,mockcpp功能強大且用戶體驗勝過gmock耐亏,所以基本沒有什么好對比和推薦的,如果需要直接上mockcpp就好了沪斟。
單元測試過程
基于前面介紹的xUnit測試框架,為代碼做單元測試的過程一般分為如下主要步驟:
- 單元測試環(huán)境搭建暇矫;
- 單元測試編寫主之、運行,測試通過后將代碼合入代碼管理倉庫(GIT或SVN)李根;
- 持續(xù)集成服務(wù)器根據(jù)規(guī)則統(tǒng)一運行所有已入庫的單元測試用例槽奕;
單元測試環(huán)境搭建
這一步是在每個開發(fā)人員的機器上搭建單元測試環(huán)境。需要做的步驟如下:
- 下載gtest和mockcpp源碼房轿,按照gtest和mockcpp的構(gòu)建安裝手冊粤攒,進(jìn)行編譯安裝;
- 針對當(dāng)前項目的構(gòu)建工具鏈和目錄結(jié)構(gòu)囱持,為單元測試編寫一個構(gòu)建腳本夯接。該腳本要能做到把被測的代碼和gtest、mockcpp以及測試用例代碼編譯構(gòu)建成一個軟件程序纷妆。該腳本需要能夠一鍵式地進(jìn)行編譯盔几、鏈接以及執(zhí)行生成的軟件程序;
- 環(huán)境搭建好之后掩幢,寫一些簡單的例子試運行一下逊拍,確定環(huán)境安裝OK上鞠;
測試編寫過程
當(dāng)開發(fā)人員的機器上已經(jīng)搭建好單元測試工具后。接下來就可以對代碼進(jìn)行單元測試了芯丧。
一般使用xUnit框架進(jìn)行單元測試主要有以下幾個過程:
建立一個單元測試的代碼文件芍阎,如果是C++的話,那就是一個普通的cpp源碼文件缨恒;
選擇需要測試的對象代碼谴咸,例如某個接口函數(shù)或者某個類。在測試文件中包含待測代碼的頭文件肿轨。
-
在測試文件里編寫測試用例寿冕,測試用例一般包含以下幾個主要部分:
- 為待測代碼準(zhǔn)備上下文環(huán)境。一般就是準(zhǔn)備待測代碼可以被調(diào)用的初始條件椒袍,例如準(zhǔn)備參數(shù)驼唱、創(chuàng)建類的對象等等;
- 調(diào)用被測代碼的接口驹暑,傳入對應(yīng)準(zhǔn)備好的參數(shù)玫恳;
- 根據(jù)可觀察的返回編寫斷言,描述期望中應(yīng)當(dāng)正確發(fā)生的事情优俘。例如接口應(yīng)該的返回值是什么京办,或者某一資源應(yīng)該發(fā)生的變化結(jié)果等等。
- 清理上下文帆焕。一般是把為該測試準(zhǔn)備的上下文清理掉惭婿,這樣做主要是為了每個測試的獨立性和不互相干擾,避免下個測試受前一個測試的上下文影響叶雹。
編寫好用例后财饥,調(diào)用測試用例的構(gòu)建腳本,編譯及執(zhí)行用例折晦,看用例是否通過钥星。
如果用例失敗看是用例的問題還是被測代碼的問題,修復(fù)直到用例通過满着。
將編寫好的用例以及修改的代碼提交到代碼管理倉庫谦炒。
通過持續(xù)集成進(jìn)行部署
一般一個大型的軟件團隊都是多人合作開發(fā)的模式,這時會通過公共的代碼管理倉庫進(jìn)行協(xié)調(diào)风喇。項了保證代碼每次修改的安全性宁改,需要搭建持續(xù)集成服務(wù)器。持續(xù)集成服務(wù)器就是安裝了持續(xù)集成軟件(例如開源的Jenkins軟件)的機器魂莫。該機器會實時監(jiān)控代碼管理倉庫透且,一旦發(fā)現(xiàn)有新的代碼提交,就會觸發(fā)一系列用戶定義的持續(xù)集成任務(wù)(參加下面的示意圖)。
以Jenkins舉例來說秽誊,常見的可配置的持續(xù)集成任務(wù)包括:
- 編譯構(gòu)建鲸沮;
- PCLint檢查;
- 運行所有單元測試锅论;
- 代碼測試覆蓋率報表生成讼溺;
- 運行其它自動化的測試用例:例如組件測試或者系統(tǒng)測試;
由于持續(xù)集成服務(wù)器時刻監(jiān)控代碼管理倉庫最易,一旦有新的代碼合入就立即執(zhí)行對應(yīng)的任務(wù):例如編譯怒坯、構(gòu)建、執(zhí)行所有單元測試用例等等藻懒。持續(xù)集成工具都支持結(jié)果通知的配置剔猿,當(dāng)存在某項任務(wù)失敗則通過看板或者郵件的方式通知指定負(fù)責(zé)人,這樣一旦有人提交的代碼造成編譯構(gòu)建失敗或者單元測試失敗嬉荆,就會立即被發(fā)現(xiàn)归敬。這樣就避免了低質(zhì)量的軟件合入到代碼倉庫后,到很晚才能知道的問題鄙早。
測試覆蓋率統(tǒng)計
和單元測試相關(guān)性較大的一個是測試覆蓋率報表的生成汪茧。對于C/C++,可選的測試覆蓋率工具并不多限番,見下表舱污。
工具 | 平臺 | 是否開源 |
---|---|---|
Coverage Validator | windows | 商用 |
OpenCppCoverage | windows | 開源 (只支持VS2013以上版本) |
gcov + lcov | linux | 開源 |
測試覆蓋率工具一般安裝部署在持續(xù)集成對應(yīng)的機器上,這樣每次持續(xù)集成服務(wù)器跑完測試用例后弥虐,就會根據(jù)當(dāng)前的測試運行情況自動計算出所有代碼的測試覆蓋率結(jié)果扩灯,可以詳細(xì)看到每一行代碼的的覆蓋情況。生成的報表可以自動發(fā)布成一個網(wǎng)頁霜瘪,項目中的所有人都可以看到驴剔。
單元測試注意事項
前面我們介紹了單元測試的工具和實施過程,接下來我們看看做好單元測試要注意的一些事項粥庄。
常見誤區(qū)
在實踐的過程中,發(fā)現(xiàn)經(jīng)常有團隊雖然開發(fā)了大量的單元測試豺妓,但是單元測試有效性卻很低惜互,付出了大量成本卻并沒有得到單元測試的收益×帐茫總結(jié)之后主要有以下一些原因:
異常測試覆蓋不足训堆;我們不需要對被測對象的所有可能輸入都做測試,但是需要對其做等價類劃分白嘁,對于每種等價類至少需要一條測試坑鱼。常見的錯誤做法是永遠(yuǎn)只測試正常場景,對異常場景測試的很少。
測試缺少斷言未状;每個測試結(jié)束后需要用斷言來設(shè)置正確的預(yù)期結(jié)果叁熔。如果斷言沒有寫全喂链,那么必然遺漏了重要的檢查點,就相當(dāng)于給安全網(wǎng)撕了個口子彭谁。見過一些極致的場景,開發(fā)人員為了完成測試用例指標(biāo)而去湊測試用例數(shù)允扇,所有用例不加斷言缠局。這樣雖然看到執(zhí)行通過的測試用例很多,測試覆蓋率也很好考润,但是全是無效用例狭园。
測試設(shè)計能力不足,測試覆蓋沒有規(guī)劃糊治。理想的情況下應(yīng)該每個測試對被測代碼的覆蓋是正交的唱矛,每個測試用例覆蓋產(chǎn)品代碼的一部分,整體上防護全部俊戳。這需要有頂層的測試設(shè)計揖赴,尤其是對于后補的單元測試,頂層的測試設(shè)計可以規(guī)劃優(yōu)先級和從重點區(qū)域開始覆蓋抑胎。常見的誤區(qū)是開發(fā)人員各自加單元測試燥滑,但是遺漏了對重要區(qū)域的覆蓋。
產(chǎn)品代碼設(shè)計問題阿逃,物理或者邏輯依賴太復(fù)雜铭拧,導(dǎo)致單元測試很難寫。這時需要對原有代碼邊重構(gòu)邊補充單元測試恃锉。所以說單元測試能否搞好搀菩,不僅僅是測試的問題,即使不采用TDD的方式也得對產(chǎn)品代碼中的不合理設(shè)計做優(yōu)化破托,才能讓單元測試更有效肪跋。
如何降低單元測試成本
從長期來看,降低單元測試的成本并不在于使用了更好的單元測試工具土砂,而在于降低由于被測代碼改動導(dǎo)致單元測試隨之變動的頻度州既。軟件之所以和硬件不同就在于它的軟,它的存在價值就是為了應(yīng)對變化萝映。而軟件的變化性往往越向內(nèi)傳遞越劇烈吴叶,這導(dǎo)致了軟件單元級別的設(shè)計經(jīng)常處于變更的核心旋渦之中。所以經(jīng)常見到有些軟件團隊序臂,一旦需求變化快工期緊蚌卤,很快就把單元測試拋棄掉了。被測代碼變化導(dǎo)致單元測試跟著變化不可能不發(fā)生,但是我們要通過設(shè)計降低這種聯(lián)動變化的概率逊彭,這樣才能降低單元測試的維護成本咸灿。
要讓單元測試能夠以較低成本維護,需要注意一下事項:
- 單元測試盡可能是單元級別的黑盒測試诫龙。白盒測試和代碼實現(xiàn)細(xì)節(jié)耦合大析显,一旦代碼修改測試就要跟著改,造成重復(fù)工作量签赃。
- 單元測試前先要理清楚被測對象的耦合關(guān)系谷异。如果被測對象的耦合關(guān)系復(fù)雜,那么測試用例需要模擬被測對象的所有耦合關(guān)系锦聊,這樣一旦被測對象的依賴關(guān)系發(fā)生變化歹嘹,測試也要跟著一起改。這時最好是先對被測代碼做一定的解耦重構(gòu)工作孔庭。
- 如果可能盡量學(xué)習(xí)并掌握TDD的做法尺上,對新開發(fā)的代碼嘗試采用TDD,讓單元測試驅(qū)動出更好的代碼實現(xiàn)圆到,反過來也驅(qū)動出了相對更穩(wěn)定的測試用例怎抛。
- 開發(fā)人員的代碼設(shè)計能力決定了單元測試的根本質(zhì)量,需要持續(xù)地提高開發(fā)人員的軟件編碼能力芽淡。
由上可見马绝,做好單元測試不只是掌握單元測試工具的使用就萬事大吉了。需要對開發(fā)人員的能力進(jìn)行提升挣菲,主要包括:
- 如何合理設(shè)計軟件富稻,對軟件單元進(jìn)行劃分,設(shè)計低耦合系統(tǒng)白胀;
- 如何設(shè)計出靠近黑盒級別的自動化單元測試用例椭赋;
- 如何對遺留系統(tǒng)進(jìn)行解耦重構(gòu)的能力;
- 掌握并實施TDD的能力或杠;
- 設(shè)計合理的持續(xù)集成策略哪怔;
將單元測試與整體測試策略結(jié)合
單元測試只是軟件測試策略中的一個環(huán)節(jié),其它的還有系統(tǒng)測試向抢,集成測試认境,組件測試等。每一級的測試都有其價值和不足笋额,所以整體測試策略需要關(guān)注如何把這些測試策略整合起來,讓整體的成本收益率最好篷扩。所以從根本上需要站在全局規(guī)劃整體的測試策略兄猩,這塊可以參考敏捷測試的測試象限和金字塔模型理論,然后根據(jù)項目的實際情況制定合理的整體測試策略。
相關(guān)推薦材料和培訓(xùn)
- 最全的xUnit開源測試框架列表枢冤,針對每種編程語言:https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks
- xUnit測試最佳實踐:《xUnit Test Patterns》可是說是最權(quán)威的一本書鸠姨。
- 測試策略規(guī)劃:《敏捷軟件測試》,測試象限和測試金字塔理論淹真。
- 《TDD in Embeded C》:嵌入式環(huán)境的TDD實踐指導(dǎo)讶迁。