單元測(cè)試
故事場(chǎng)景
工廠生產(chǎn)電視機(jī)
工廠首先會(huì)將各種電子元器件按照?qǐng)D紙組裝在一起構(gòu)成各個(gè)功能電路板,比如供電板锈死、音視頻解碼板贫堰、射頻接收板等,然后再將這些電路板組裝起來構(gòu)成一個(gè)完整的電視機(jī)待牵。
如果一切順利其屏,接通電源后,你就可以開始觀看電視節(jié)目了缨该。但是很不幸偎行,大多數(shù)情況下組裝完成的電視機(jī)根本無法開機(jī),這時(shí)你就需要把電視機(jī)拆開,然后逐個(gè)模塊排查問題蛤袒。
假設(shè)你發(fā)現(xiàn)是供電板的供電電壓不足熄云,那你就要繼續(xù)逐級(jí)排查組成供電板的各個(gè)電子元器件,最終你可能發(fā)現(xiàn)罪魁禍?zhǔn)资且粋€(gè)電容的故障妙真。這時(shí)缴允,為了定位到這個(gè)問題,可能已經(jīng)花費(fèi)了大量的時(shí)間和精力珍德。
如何避免练般?
如何才能避免類似的問題呢?
為什么不在組裝前锈候,就先測(cè)試每個(gè)要用到的電子元器件呢薄料?這樣就可以先排除有問題的元器件,最大程度地防止組裝完成后逐級(jí)排查問題的事情發(fā)生泵琳。
單元測(cè)試 VS 工廠生產(chǎn)電視機(jī)
如果把電視機(jī)的生產(chǎn)摄职、測(cè)試和軟件的開發(fā)、測(cè)試進(jìn)行類比,可以發(fā)現(xiàn):
- 電子元器件就像是軟件中的單元,通常是函數(shù)或者類贞铣,對(duì)單個(gè)元器件的測(cè)試就像是軟件測(cè)試中的單元測(cè)試嘶朱;
- 組裝完成的功能電路板就像是軟件中的模塊,對(duì)電路板的測(cè)試就像是軟件中的集成測(cè)試;
- 電視機(jī)全部組裝完成就像是軟件完成了預(yù)發(fā)布版本,電視機(jī)全部組裝完成后的開機(jī)測(cè)試就像是軟件中的系統(tǒng)測(cè)試。
通過類比及皂,可以發(fā)現(xiàn)單元測(cè)試的重要性。那么單元測(cè)試到底是什么呢且改?
單元測(cè)試-基本概念
單元測(cè)試是指验烧,對(duì)軟件中的最小可測(cè)試單元在與程序其他部分相隔離的情況下進(jìn)行檢查和驗(yàn)證的工作,最小可測(cè)試單元通常是指函數(shù)或者類又跛。
維基百科中這樣定義:
單元測(cè)試(Unit Testing)又稱為模塊測(cè)試碍拆,是針對(duì)程序模塊來進(jìn)行正確性檢驗(yàn)的測(cè)試工作。在過程化編程中慨蓝,一個(gè)單元就是單個(gè)程序感混、函數(shù)、過程等礼烈;對(duì)于面向?qū)ο缶幊袒÷钚卧褪欠椒ǎɑ悾ǔ悾┐税尽⒊橄箢愅ノ亍⒒蛘吲缮悾ㄗ宇悾┲械姆椒ā?/p>
其實(shí)滑进,對(duì)“單元”的定義取決于自己。如果正在使用函數(shù)式編程募谎,一個(gè)單元最有可能指的是一個(gè)函數(shù)扶关,單元測(cè)試將使用不同的參數(shù)調(diào)用這個(gè)函數(shù),并斷言它返回了期待的結(jié)果数冬;在面向?qū)ο笳Z言里节槐,下至一個(gè)方法,上至一個(gè)類都可以是一個(gè)單元拐纱。
單元測(cè)試-金字塔模型
冰淇淋模型
在金字塔模型之前疯淫,流行的是冰淇淋模型。包含了大量的手工測(cè)試戳玫、端到端的自動(dòng)化測(cè)試及少量的單元測(cè)試。造成的后果是未斑,隨著產(chǎn)品壯大咕宿,手工回歸測(cè)試時(shí)間越來越長,質(zhì)量很難把控蜡秽;自動(dòng)化 case 經(jīng)常失敗府阀,每一個(gè)失敗對(duì)應(yīng)著一個(gè)很長的函數(shù)調(diào)用。
哪里出了問題芽突?
- 單元測(cè)試太少试浙,基本沒起作用。
金字塔模型
”測(cè)試金字塔“ 比喻非常形象寞蚌,讓人一眼就知道測(cè)試是需要分層的田巴,并且還告訴你每一層需要寫多少測(cè)試。測(cè)試金字塔具備兩點(diǎn)經(jīng)驗(yàn)法則:
- 編寫不同粒度的測(cè)試
- 層次越高挟秤,寫的測(cè)試應(yīng)該越少
可以把金字塔模型理解為——冰激凌融化了壹哺。就是指,最頂部的“手工測(cè)試”理論上全部要自動(dòng)化艘刚,向下融化管宵,優(yōu)先全部考慮融化成單元測(cè)試,單元測(cè)試覆蓋不了的放在中間層(分層測(cè)試)攀甚,再覆蓋不了的才會(huì)放到 UI 層箩朴。 因此,不分單元測(cè)試還是分層測(cè)試秋度,統(tǒng)一都叫自動(dòng)化測(cè)試炸庞,把所有的自動(dòng)化 case 看做一個(gè)整體,case不要冗余静陈,單元測(cè)試能覆蓋燕雁,就要把這個(gè)case從分層或ui中去掉诞丽。越是底層的測(cè)試,牽扯到相關(guān)內(nèi)容越少拐格,而高層測(cè)試則涉及面更廣僧免。
單元測(cè)試:它的關(guān)注點(diǎn)只有一個(gè)單元,而沒有其它任何東西捏浊。所以懂衩,只要一個(gè)單元寫好了,測(cè)試就是可以通過的
集成測(cè)試:要把好幾個(gè)單元組裝到一起才能測(cè)試金踪,測(cè)試通過的前提條件是浊洞,所有這些單元都寫好了,這個(gè)周期就明顯比單元測(cè)試要長
系統(tǒng)測(cè)試:要把整個(gè)系統(tǒng)的各個(gè)模塊都連在一起胡岔,各種數(shù)據(jù)都準(zhǔn)備好法希,才可能通過。另外靶瘸,因?yàn)樯婕暗降哪K過多苫亦,任何一個(gè)模塊做了調(diào)整,都有可能破壞高層測(cè)試
單元測(cè)試-意義
- 在開發(fā)早期以最小的成本保證局部代碼的質(zhì)量
- 在單元測(cè)試代碼里提供函數(shù)的使用示例
- 實(shí)施過程中幫助開發(fā)工程師改善代碼的設(shè)計(jì)與實(shí)現(xiàn)
- 單元測(cè)試都是以自動(dòng)化的方式執(zhí)行怨咪,在大量回歸測(cè)試的場(chǎng)景下更能帶來高收益
如何做好單元測(cè)試
- 明確單元測(cè)試的對(duì)象是代碼屋剑,代碼的基本特征和產(chǎn)生錯(cuò)誤的原因
- 對(duì)單元測(cè)試的用例設(shè)計(jì)有深入的理解
- 掌握單元測(cè)試的基本方法和主要技術(shù)手段 - 驅(qū)動(dòng)代碼、樁代碼和 Mock 代碼等
代碼的基本特征與產(chǎn)生錯(cuò)誤的原因
拋開業(yè)務(wù)邏輯诗眨,從代碼結(jié)構(gòu)來看:
所有的代碼無異于條件分支唉匾、循環(huán)處理和函數(shù)調(diào)用等最基本的邏輯控制,都是在對(duì)數(shù)據(jù)進(jìn)行分類處理匠楚,每一次條件判定都是一次分類處理巍膘。
- 如果有任何一個(gè)分類遺漏,都會(huì)產(chǎn)生缺陷
- 如果有任何一個(gè)分類錯(cuò)誤芋簿,也會(huì)產(chǎn)生缺陷
- 如果分類正確也沒有遺漏典徘,但是分類時(shí)的處理邏輯錯(cuò)誤,也同樣會(huì)產(chǎn)生缺陷
對(duì)單元測(cè)試的用例設(shè)計(jì)有深入的理解
單元測(cè)試的用例是一個(gè)“輸入數(shù)據(jù)”和“預(yù)計(jì)輸出”的集合益咬。就是在明確了代碼需要實(shí)現(xiàn)的邏輯功能的基礎(chǔ)上逮诲,什么輸入,應(yīng)該產(chǎn)生什么輸出幽告。但是會(huì)存在下面的誤區(qū):
誤區(qū):
- 輸入:只有被測(cè)試函數(shù)的輸入?yún)?shù)是“輸入數(shù)據(jù)”
- 輸出:只有函數(shù)返回值是”輸出數(shù)據(jù)“
完整的單元測(cè)試“輸入數(shù)據(jù)”
- 被測(cè)試函數(shù)的輸入?yún)?shù)~~~~
- 被測(cè)試函數(shù)內(nèi)部需要讀取的全局靜態(tài)變量
- 被測(cè)試函數(shù)內(nèi)部需要讀取的成員變量
- 函數(shù)內(nèi)部調(diào)用子函數(shù)獲得的數(shù)據(jù)
- 函數(shù)內(nèi)部調(diào)用子函數(shù)改寫的數(shù)據(jù)
- 嵌入式系統(tǒng)中梅鹦,在中斷調(diào)用時(shí)改寫的數(shù)據(jù)
- ...
完整的單元測(cè)試“輸出數(shù)據(jù)”
- 被測(cè)試函數(shù)的返回值
- 被測(cè)試函數(shù)的輸出參數(shù)
- 被測(cè)試函數(shù)所改寫的成員變量
- 被測(cè)試函數(shù)所改寫的全局變量
- 被測(cè)試函數(shù)中進(jìn)行的文件更新
- 被測(cè)試函數(shù)中進(jìn)行的數(shù)據(jù)庫更新
- 被測(cè)試函數(shù)中進(jìn)行的消息隊(duì)列更新
- ...
掌握單元測(cè)試的基本方法和主要技術(shù)手段 - 驅(qū)動(dòng)代碼、樁代碼和 Mock 代碼等
- 驅(qū)動(dòng)代碼(Driver): 指調(diào)用被測(cè)函數(shù)的代碼冗锁,通常包括了被測(cè)函數(shù)前的數(shù)據(jù)準(zhǔn)備(如 @Before 修飾的代碼)齐唆、調(diào)用被測(cè)函數(shù)以及驗(yàn)證相關(guān)結(jié)果,結(jié)構(gòu)通常由單元測(cè)試框架決定
- 樁代碼(Stub): 是用來代替真實(shí)代碼的臨時(shí)代碼
- Mock 代碼: 和樁代碼非常類似冻河,都是用來代替真實(shí)代碼的臨時(shí)代碼箍邮,起到隔離和補(bǔ)齊的作用茉帅,和樁代碼的本質(zhì)區(qū)別是:測(cè)試期待結(jié)果的驗(yàn)證(Assert and Expectiation)。
樁代碼-被測(cè)函數(shù)
示例:函數(shù)A是被測(cè)函數(shù)锭弊,內(nèi)部調(diào)用了函數(shù)B
void funcA(){
boolean funcB_retVal = funcB();
if (true == funcB_retV){
do Operation 1;
}else{
do Operation 2;
}
}
樁代碼-樁函數(shù)
在單元測(cè)試階段堪澎,由于函數(shù)B尚未實(shí)現(xiàn),但是為了不影響對(duì)函數(shù)A的測(cè)試味滞,可以用一個(gè)假的函數(shù)B來代替真實(shí)的函數(shù)B樱蛤,那么這個(gè)假的函數(shù)B就是樁函數(shù)。
并且剑鞍,為實(shí)現(xiàn)函數(shù)A的全路徑覆蓋昨凡,需要控制不同的測(cè)試用例中函數(shù)B的返回值,代碼如下:
boolean funcB(){
if(testCaseID == 'TC0001'){
return true;
}else if(testCaseID == 'TC0002'){
return false;
}
}
當(dāng)執(zhí)行第一個(gè)測(cè)試用例的時(shí)候蚁署,樁函數(shù)B應(yīng)該返回true便脊,而當(dāng)執(zhí)行第二個(gè)測(cè)試用例的時(shí)候,樁函數(shù)B應(yīng)該返回false光戈。
樁代碼原則
- 具有與原函數(shù)完全相同的原形就轧,僅僅是內(nèi)部實(shí)現(xiàn)不同
- 用于隔離和補(bǔ)齊的樁函數(shù),只需保持原函數(shù)聲明田度,加一個(gè)空實(shí)現(xiàn),目的是通過編譯鏈接
- 控制功能的樁函數(shù)要根據(jù)測(cè)試用例的需要解愤,輸出合適的數(shù)據(jù)作為被測(cè)函數(shù)的輸入
同時(shí)镇饺,樁代碼關(guān)注點(diǎn)是利用 Stub 來控制被測(cè)函數(shù)的執(zhí)行路徑,不會(huì)去關(guān)注 Stub 是否被調(diào)用以及怎樣被調(diào)用送讲。
Mock 代碼
關(guān)注點(diǎn):
- Mock 方法有沒有被調(diào)用
- 以什么樣的參數(shù)被調(diào)用
- 被調(diào)用的次數(shù)
- 多個(gè) Mock 函數(shù)的先后調(diào)用順序
- ...
所以奸笤,在使用Mock代碼的測(cè)試中,對(duì)于結(jié)果的驗(yàn)證(也就是assert)哼鬓,通常出現(xiàn)在 Mock 函數(shù)中
Mock 測(cè)試
背景
對(duì)于持續(xù)交付中的測(cè)試來說监右,自動(dòng)化回歸測(cè)試不可或缺,但存在如下三個(gè)難點(diǎn):
- 測(cè)試數(shù)據(jù)的準(zhǔn)備和清理
- 分布式系統(tǒng)的依賴
- 測(cè)試用例高度仿真
解決方案:
- Mock
- “回放”技術(shù)(記錄實(shí)際用戶在生產(chǎn)環(huán)境的操作异希,然后在測(cè)試環(huán)境中回放)
- 攔截:
- SLB 統(tǒng)一做攔截和復(fù)制轉(zhuǎn)發(fā)處理健盒;主路徑影響路由,容易故障
- 集群擴(kuò)容一臺(tái)軟交換服務(wù)器称簿,負(fù)責(zé)復(fù)制和轉(zhuǎn)發(fā)用戶請(qǐng)求扣癣;
- 回放
- 攔截:
Mock 背景 - 分布式系統(tǒng)依賴
微服務(wù)項(xiàng)目中會(huì)出現(xiàn)相互依賴的關(guān)系,比如由于服務(wù) B 依賴服務(wù) C憨降,而服務(wù) C 還沒有開發(fā)完成父虑,導(dǎo)致即使服務(wù) A 和服務(wù) B 都沒問題,但也沒有辦法完成服務(wù) A 的接口測(cè)試授药。
還有的會(huì)依賴數(shù)據(jù)庫士嚎、消息中間件等:
Mock 基本概念介紹
Mock 測(cè)試就是在測(cè)試過程中呜魄,對(duì)于某些不容易構(gòu)造或者不容易獲取的對(duì)象,用一個(gè)虛擬的對(duì)象來創(chuàng)建以便測(cè)試的測(cè)試方法莱衩。
好處
團(tuán)隊(duì)并行工作
團(tuán)隊(duì)間不需互相等待對(duì)方進(jìn)度爵嗅,只需約定好相互之間的數(shù)據(jù)規(guī)范(接口文檔),即可使用 mock 構(gòu)建出可用接口膳殷,然后盡快進(jìn)行開發(fā)和自測(cè)操骡,提前發(fā)現(xiàn)缺陷測(cè)試驅(qū)動(dòng)開發(fā) TDD (Test-Driven Development)
單元測(cè)試是 TDD 實(shí)現(xiàn)的基石,而 TDD 經(jīng)常會(huì)碰到協(xié)同模塊尚未開發(fā)完成的情況赚窃,但有了 mock册招,當(dāng)接口定義好后,測(cè)試人員就可以創(chuàng)建一個(gè) Mock勒极,把接口添加到自動(dòng)化測(cè)試環(huán)境是掰,提前創(chuàng)建測(cè)試。測(cè)試覆蓋率
若一個(gè)接口在不同的狀態(tài)下要返回不同的值辱匿,常見做法是復(fù)現(xiàn)這種狀態(tài)然后再去請(qǐng)求接口键痛,而這種方法很可能因操作時(shí)機(jī)或方式不當(dāng)導(dǎo)致失敗,甚至污染后端存儲(chǔ)如數(shù)據(jù)庫等, 但用 mock 則不用擔(dān)心隔離系統(tǒng)
使用某些接口時(shí)匾七,為避免系統(tǒng)數(shù)據(jù)庫被污染絮短,可以將接口調(diào)整為 Mock 模式,以保證數(shù)據(jù)庫純凈昨忆。方便演示
Mock 框架介紹
Mock 技術(shù)主要的應(yīng)用場(chǎng)景可以分為兩類:
-
基于對(duì)象和類的 Mock
- Mockito & PowerMock
-
基于微服務(wù)的 Mock
- Moco丁频、MockMVC、WireMock邑贴、Mock Server
因?yàn)轫?xiàng)目主要基于 Java 開發(fā), 因此下面主要介紹 Java 相關(guān)的 Mock 框架, 其他語言思想類似
基于對(duì)象和類的 Mock
- 原理:
- 在運(yùn)行時(shí)席里,為每一個(gè)被 Mock 的對(duì)象或類動(dòng)態(tài)生成一個(gè)代理對(duì)象,由這個(gè)代理對(duì)象返回預(yù)先設(shè)計(jì)的結(jié)果
- 場(chǎng)景:
- 適合模擬 DAO 層的數(shù)據(jù)操作和復(fù)雜邏輯,常用于用于單元測(cè)試階段
基于微服務(wù)的 Mock
從代碼編寫的角度來看拢驾,實(shí)現(xiàn)方式如下:
- 聲明被代理的服務(wù)
- 通過 Mock 框架定制代理的行為
- 調(diào)用代理奖磁,從而獲得預(yù)期的結(jié)果
Mockito & PowerMock ★★
Mockito 是 GitHub 上使用非常廣泛的 Java Mock 框架, star 數(shù) 11k, 在包括 openstack4j 和 kubernetes-client/java 等都有用到。Mockito 與 JUnit 結(jié)合使用, 能隔離外部依賴以便對(duì)自己的業(yè)務(wù)邏輯代碼進(jìn)行單元測(cè)試在編寫單元測(cè)試需要調(diào)用某一個(gè)接口時(shí)繁疤,可以模擬一個(gè)假方法咖为,并任意指定方法的返回值。
但缺點(diǎn)是 Mockito 2 版本對(duì)靜態(tài)方法稠腊、final 方法案疲、private 方法和構(gòu)造函數(shù)的功能支持并不完善, 因此 PowerMock 則在 Mockito 原有的基礎(chǔ)上做了擴(kuò)展,通過修改類字節(jié)碼并使用自定義 ClassLoader 加載運(yùn)行的方式來實(shí)現(xiàn) mock 靜態(tài)方法麻养、final 方法褐啡、private 方法和構(gòu)造函數(shù)等功能。
Mockito & PowerMock 一般測(cè)試步驟
1. mock: 模擬對(duì)象
用 mock()/@Mock 或 spy()/@Spy 創(chuàng)建模擬對(duì)象, 兩者創(chuàng)建出來的模擬對(duì)象區(qū)別是: 使用 mock 生成的對(duì)象鳖昌,所有方法都是被 mock 的备畦,除非某個(gè)方法被 stub 了低飒,否則返回值都是默認(rèn)值; 使用 spy 生產(chǎn)的 spy 對(duì)象,所有方法都是調(diào)用的 spy 對(duì)象的真實(shí)方法懂盐,直到某個(gè)方法被 stub 后
2. stub: 定義樁函數(shù)
可以通過 when()/given()/thenReturn()/doReturn()/thenAnswer() 等來定義 mock 對(duì)象如何執(zhí)行, 如果提供的接口不符合需求, 還可以通過實(shí)現(xiàn) Answer 接口來自定義實(shí)現(xiàn)
3. run: 執(zhí)行調(diào)用
執(zhí)行實(shí)際方法的調(diào)用褥赊,此時(shí)被 mock 的對(duì)象將返回自定義的樁函數(shù)的返回值
4. verify: 可選, 對(duì)調(diào)用進(jìn)行驗(yàn)證, 如是否被調(diào)用, 調(diào)用次數(shù)等
這一步可以對(duì) mock 對(duì)象的方法是否被調(diào)用以及被調(diào)用次數(shù)進(jìn)行驗(yàn)證,同時(shí)還可以對(duì)參數(shù)捕獲進(jìn)行參數(shù)校驗(yàn)
下面以操作 Redis 和 RabbitMQ 來進(jìn)行簡單舉例莉恼。
首先引入依賴:
<powermock.version>2.0.2</powermock.version>
...
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
Redis 示例
// redis 操作類
class RedisDemo {
private Jedis jedis;
public void setUp() {
jedis = new Jedis("127.0.0.1", 6379);
jedis.connect();
}
public boolean isAdmin(String user) {
String ret = jedis.get("name");
if (user.equals(ret)) {
return true;
}
return false;
}
public void set(String key, String val) {
jedis.set(key, val);
}
public String get(String key) {
String s = jedis.get(key);
return s;
}
void out(){
System.out.println("ss");
}
}
// 單元測(cè)試類
@RunWith(PowerMockRunner.class) //讓測(cè)試運(yùn)行于PowerMock環(huán)境
public class RedisMockitoTest {
@Mock //此注解會(huì)自動(dòng)創(chuàng)建1個(gè)mock對(duì)象并注入到@InjectMocks對(duì)象中
private Jedis jedis;
@InjectMocks
private RedisDemo demo;
@Mock
StringOperator stringOperator;
//第1種方式
@Test
public void redisTest1() throws Exception {
Mockito.when(jedis.get("name")).thenReturn("admin");
boolean admin = demo.isAdmin("admin");
assertTrue(admin);
}
//第2種方式
@Test
public void redisTest2() {
RedisDemo demo = mock(RedisDemo.class);
ReflectionTestUtils.setField(demo, "jedis", jedis);
when(demo.isAdmin("admin")).thenReturn(true);
boolean admin = demo.isAdmin("admin");
assertTrue(admin);
}
//第3種方式
@Test
public void redisTest3() {
RedisDemo demo = mock(RedisDemo.class);
doReturn(true).when(demo).isAdmin("admin");
System.out.println(demo.isAdmin("admin"));
}
}
RabbitMQ 示例
@Component
public class DirectReceiver {
@Autowired
RabbitTemplate rabbitTemplate;
public Object getMsg() {
return rabbitTemplate.receiveAndConvert("queue_demo");
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = Main.class)
public class RecvMessage {
@Spy
RabbitTemplate rabbitTemplate;
@InjectMocks
@Autowired
DirectReceiver receiver;
@Test
public void recvTest() {
doReturn("Mock answer").when(rabbitTemplate).receiveAndConvert("queue_demo");
System.out.println(rabbitTemplate.receiveAndConvert("queue_demo"));
}
}
更多示例
public class Node {
private int num;
private String name;
public static Node getStaticNode() {
return new Node(1, "static node");
}
public Node() {
}
public Node(String name) {
this.name = name;
}
public Node(int num) {
this.num = num;
}
public Node(int num, String name) {
this.num = num;
this.name = name;
}
}
public class LocalServiceImpl implements ILocalService {
@Autowired
private IRemoteService remoteService;
@Override
public Node getLocalNode(int num, String name) {
return new Node(num, name);
}
@Override
public Node getRemoteNode(int num) {
return remoteService.getRemoteNode(num);
}
@Override
public Node getRemoteNode(String name) throws MockException {
try {
return remoteService.getRemoteNode(name);
} catch (IllegalArgumentException e) {
throw e;
}
}
@Override
public void remoteDoSomething() {
remoteService.doSometing();
}
}
public class RemoteServiceImpl implements IRemoteService {
@Override
public Node getRemoteNode(int num) {
return new Node(num, "Node from remote service");
}
@Override
public final Node getFinalNode() {
return new Node(1, "final node");
}
@Override
public Node getRemoteNode(String name) throws MockException {
if (StringUtils.isEmpty(name)) {
throw new MockException("name不能為空", name);
}
return new Node(name);
}
@Override
public void doSometing() {
System.out.println("remote service do something!");
}
@Override
public Node getPrivateNode() {
return privateMethod();
}
private Node privateMethod() {
return new Node(1, "private node");
}
@Override
public Node getSystemPropertyNode() {
return new Node(System.getProperty("abc"));
}
}
// 單元測(cè)試類
@RunWith(MockitoJUnitRunner.class) //讓測(cè)試運(yùn)行于Mockito環(huán)境
public class LocalServiceImplMockTest {
@InjectMocks //此注解表示這個(gè)對(duì)象需要被注入mock對(duì)象
private LocalServiceImpl localService;
@Mock //此注解會(huì)自動(dòng)創(chuàng)建1個(gè)mock對(duì)象并注入到@InjectMocks對(duì)象中
private RemoteServiceImpl remoteService;
@Captor
private ArgumentCaptor<String> localCaptor;
//如果不使用上述注解拌喉,可以使用@Before方法來手動(dòng)進(jìn)行mock對(duì)象的創(chuàng)建和注入,但會(huì)多幾行代碼
/*@Before
public void setUp() throws Exception {
localService = new LocalServiceImpl();
remoteService = mock(RemoteServiceImpl.class);
Whitebox.setInternalState(localService, "remoteService", remoteService);
}*/
/**
* any系列方法指定多參數(shù)情況
*/
@Test
public void testAny() {
Node target = new Node(1, "target");
when(remoteService.getRemoteNode(anyInt())).thenReturn(target); //靜態(tài)導(dǎo)入Mockito.when和ArgumentMatchers.anyInt后可以簡化代碼提升可讀性
Node result = localService.getRemoteNode(20); //上面指定了調(diào)用remoteService.getRemoteNode(int)時(shí)俐银,不管傳入什么參數(shù)都會(huì)返回target對(duì)象
assertEquals(target, result); //可以斷言我們得到的返回值其實(shí)就是target對(duì)象
assertEquals(1, result.getNum()); //具體屬性和我們指定的返回值相同
assertEquals("target", result.getName()); //具體屬性和我們指定的返回值相同
}
/**
* 指定mock多次調(diào)用返回值
*/
@Test
public void testMultipleReturn() {
Node target1 = new Node(1, "target");
Node target2 = new Node(1, "target");
Node target3 = new Node(1, "target");
when(remoteService.getRemoteNode(anyInt())).thenReturn(target1).thenReturn(target2).thenReturn(target3);
//第一次調(diào)用返回target1尿背、第二次返回target2、第三次返回target3
Node result1 = localService.getRemoteNode(1); //第1次調(diào)用
assertEquals(target1, result1);
Node result2 = localService.getRemoteNode(2); //第2次調(diào)用
assertEquals(target2, result2);
Node result3 = localService.getRemoteNode(3); //第3次調(diào)用
assertEquals(target3, result3);
}
/**
* 指定mock對(duì)象已聲明異常拋出的方法拋出受檢查異常
*/
@Test
public void testCheckedException() {
try {
Node target = new Node(1, "target");
when(remoteService.getRemoteNode("name")).thenReturn(target).thenThrow(new MockException("message", "exception")); //第一次調(diào)用正常返回捶惜,第二次則拋出一個(gè)Exception
Node result1 = localService.getRemoteNode("name");
assertEquals(target, result1); //第一次調(diào)用正常返回
Node result2 = localService.getRemoteNode("name"); //第二次調(diào)用不會(huì)正常返回田藐,會(huì)拋出異常
assertEquals(target, result2);
} catch (MockException e) {
assertEquals("exception", e.getName()); //驗(yàn)證是否返回指定異常內(nèi)容
assertEquals("message", e.getMessage()); //驗(yàn)證是否返回指定異常內(nèi)容
}
}
/**
* 校驗(yàn)mock對(duì)象和方法的調(diào)用情況
*/
public void testVerify() {
Node target = new Node(1, "target");
when(remoteService.getRemoteNode(anyInt())).thenReturn(target);
verify(remoteService, Mockito.never()).getRemoteNode(1); //mock方法未調(diào)用過
localService.getRemoteNode(1);
verify(remoteService, times(1)).getRemoteNode(anyInt()); //目前mock方法調(diào)用過1次
localService.getRemoteNode(2);
verify(remoteService, times(2)).getRemoteNode(anyInt()); //目前mock方法調(diào)用過2次
verify(remoteService, times(1)).getRemoteNode(2); //目前mock方法參數(shù)為2只調(diào)用過1次
}
/**
* mock對(duì)象調(diào)用真實(shí)方法
*/
@Test
public void testCallRealMethod() {
when(remoteService.getRemoteNode(anyInt())).thenCallRealMethod(); //設(shè)置調(diào)用真實(shí)方法
Node result = localService.getRemoteNode(1);
assertEquals(1, result.getNum());
assertEquals("Node from remote service", result.getName());
}
/**
* 利用ArgumentCaptor捕獲方法參數(shù)進(jìn)行mock方法參數(shù)校驗(yàn)
*/
@Test
public void testCaptor() throws Exception {
Node target = new Node(1, "target");
when(remoteService.getRemoteNode(anyString())).thenReturn(target);
localService.getRemoteNode("name1");
localService.getRemoteNode("name2");
verify(remoteService, atLeastOnce()).getRemoteNode(localCaptor.capture()); //設(shè)置captor
assertEquals("name2", localCaptor.getValue()); //獲取最后一次調(diào)用的參數(shù)
List<String> list = localCaptor.getAllValues(); //按順序獲取所有傳入的參數(shù)
assertEquals("name1", list.get(0));
assertEquals("name2", list.get(1));
}
/**
* 校驗(yàn)mock對(duì)象0調(diào)用和未被驗(yàn)證的調(diào)用
*/
@Test(expected = NoInteractionsWanted.class)
public void testInteraction() {
verifyZeroInteractions(remoteService); //目前還未被調(diào)用過,執(zhí)行不報(bào)錯(cuò)
Node target = new Node(1, "target");
when(remoteService.getRemoteNode(anyInt())).thenReturn(target);
localService.getRemoteNode(1);
localService.getRemoteNode(2);
verify(remoteService, times(2)).getRemoteNode(anyInt());
// 參數(shù)1和2的兩次調(diào)用都會(huì)被上面的anyInt()校驗(yàn)到吱七,所以沒有未被校驗(yàn)的調(diào)用了
verifyNoMoreInteractions(remoteService);
reset(remoteService);
localService.getRemoteNode(1);
localService.getRemoteNode(2);
verify(remoteService, times(1)).getRemoteNode(1);
// 參數(shù)2的調(diào)用不會(huì)被上面的校驗(yàn)到汽久,所以執(zhí)行會(huì)拋異常
verifyNoMoreInteractions(remoteService);
}
}
Moco
Moco 框架在開發(fā) Mock 服務(wù)的時(shí)候提供了一種不需任何編程語言的方式, 可以通過撰寫它約束的 json 建立服務(wù), 并通過命令獨(dú)立啟動(dòng)對(duì)應(yīng)的服務(wù), 這可以快速開發(fā)和啟動(dòng)運(yùn)行所需的 Mock 服務(wù). 除此之外, 也可以編寫服務(wù)代碼來進(jìn)行測(cè)試. 下面進(jìn)行簡單舉例:
- 使用 json 配置文件啟動(dòng) mock 服務(wù)
# foo.json
[
{
"response" :
{
"text" : "Hello, Moco"
}
}
]
java -jar moco-runner-1.1.0-standalone.jar http -p 12306 -c foo.json
這時(shí)訪問 http://localhost:12306/
將會(huì)返回 Hello, Moco
。
- 在項(xiàng)目中使用 Moco Java API
除了使用 json 配置文件作為獨(dú)立服務(wù)啟動(dòng)外, 還可以使用 Java API 來啟動(dòng) mock 服務(wù), 下面是代碼片段:
首先引入依賴:
<dependency>
<groupId>com.github.dreamhead</groupId>
<artifactId>moco-core</artifactId>
<version>1.1.0</version>
</dependency>
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MockServletContext.class)
public class MockAPITest {
@Test
public void should_response_as_expected() throws Exception {
HttpServer server = httpServer(12307);
server.response("foo");
running(server, new Runnable() {
@Override
public void run() throws IOException {
CloseableHttpResponse response = HttpClients.createDefault().execute(new HttpGet("http://localhost:12307"));
String content = EntityUtils.toString(response.getEntity(), "UTF-8");
assertThat(content, is("foo"));
}
});
}
}
Moco 還支持 HTTPS 和 Socket, 支持與 JUnit 集成等, 詳細(xì)內(nèi)容見文檔使用說明踊餐。
MockMVC ★
MockMVC 是 spring-boot-starter-test
包自帶的 Mock API景醇,MockMvc 實(shí)現(xiàn)了對(duì) Http 請(qǐng)求的模擬,可以方便對(duì) Controller 進(jìn)行測(cè)試吝岭,測(cè)試速度快三痰、不依賴網(wǎng)絡(luò)環(huán)境,且提供了驗(yàn)證的工具苍碟。下面是具體示例:
HelloController
//HelloController
@RestController
public class HelloController {
@RequestMapping("/hello")
public String index() {
return "Hello World";
}
}
UserController
//UserController
@Slf4j
@RestController
@RequestMapping(value = "/users") // 通過這里配置使下面的映射都在/users下
public class UserController {
// 創(chuàng)建線程安全的Map
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@RequestMapping(value = "/", method = RequestMethod.GET)
public List<User> getUserList() {
// 處理"/users/"的GET請(qǐng)求,用來獲取用戶列表
// 還可以通過@RequestParam從頁面中傳遞參數(shù)來進(jìn)行查詢條件或者翻頁信息的傳遞
List<User> r = new ArrayList<User>(users.values());
return r;
}
@RequestMapping(value = "/", method = RequestMethod.POST)
public String postUser(@ModelAttribute User user) {
// 處理"/users/"的POST請(qǐng)求撮执,用來創(chuàng)建User
// 除了@ModelAttribute綁定參數(shù)之外微峰,還可以通過@RequestParam從頁面中傳遞參數(shù)
users.put(user.getId(), user);
return "success";
}
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
public User getUser(@PathVariable Long id) {
// 處理"/users/{id}"的GET請(qǐng)求,用來獲取url中id值的User信息
// url中的id可通過@PathVariable綁定到函數(shù)的參數(shù)中
return users.get(id);
}
@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
public String putUser(@PathVariable Long id, @ModelAttribute User user) {
// 處理"/users/{id}"的PUT請(qǐng)求抒钱,用來更新User信息
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
// 處理"/users/{id}"的DELETE請(qǐng)求蜓肆,用來刪除User
users.remove(id);
return "success";
}
// 測(cè)試
@RequestMapping(value = "/postByJson", method = RequestMethod.POST)
public String postByJson(@RequestBody User user, String method) {
log.info("user: {}; method: {}", user, method);
return "success";
}
}
- 單元測(cè)試類
HttpMockTest
public class HttpMockTest {
private MockMvc mvc;
private final static ObjectMapper objectMapper = new ObjectMapper();
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.standaloneSetup(
new HelloController(),
new UserController()).build();
}
@Test
public void getHello() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World")));
}
@Test
public void testUserController() throws Exception {
// 測(cè)試UserController
RequestBuilder request = null;
// 1、get查一下user列表谋币,應(yīng)該為空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
// 2仗扬、post提交一個(gè)user
request = post("/users/")
.param("id", "1")
.param("name", "測(cè)試大師")
.param("age", "20");
mvc.perform(request)
.andDo(MockMvcResultHandlers.print())
.andExpect(content().string(equalTo("success")));
// 3、get獲取user列表蕾额,應(yīng)該有剛才插入的數(shù)據(jù)
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[{\"id\":1,\"name\":\"測(cè)試大師\",\"age\":20}]")));
// 4早芭、put修改id為1的user
request = put("/users/1")
.param("name", "測(cè)試終極大師")
.param("age", "30");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 5、get一個(gè)id為1的user
request = get("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("{\"id\":1,\"name\":\"測(cè)試終極大師\",\"age\":30}")));
// 6诅蝶、del刪除id為1的user
request = delete("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 7退个、get查一下user列表募壕,應(yīng)該為空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
// 8、json作為參數(shù)
request = post("/users/postByJson")
.param("method", "postByJson")
.content(objectMapper.writeValueAsString(new User(1L, "USER", 23)))
.contentType(MediaType.APPLICATION_JSON);
mvc.perform(request).andExpect(status().is(200))
.andExpect(content().string("success"));
}
}
WireMock ★★
WireMock 是在閱讀 kubernetes-client/java 代碼時(shí)發(fā)現(xiàn)的, 在其中有大量使用语盈,它是基于 HTTP API 的 mock 服務(wù)框架舱馅,和前面提到的 moco 一樣,它可以通過文件配置以獨(dú)立服務(wù)啟動(dòng), 也可以通過代碼控制刀荒,同時(shí) Spring Cloud Contract WireMock 模塊也使得可以在 Spring Boot 應(yīng)用中使用 WireMock代嗤,具體介紹見 Spring Cloud Contract WireMock 。除此之外, WireMock 還提供了在線 mock 服務(wù) MockLab 缠借。下面是 WireMock 在 K8S API 上的示例:
public class K8SApiTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(8000);
private GenericKubernetesApi<V1Job, V1JobList> jobClient;
ApiClient apiClient;
@Before
public void setup() {
apiClient = new ClientBuilder().setBasePath("http://localhost:" + 8000).build();
jobClient =
new GenericKubernetesApi<>(V1Job.class, V1JobList.class, "batch", "v1", "jobs", apiClient);
}
// test delete
@Test
public void delJob() {
V1Status status = new V1Status().kind("Status").code(200).message("good!");
stubFor(
delete(urlEqualTo("/apis/batch/v1/namespaces/default/jobs/foo1"))
.willReturn(aResponse().withStatus(200).withBody(new Gson().toJson(status))));
KubernetesApiResponse<V1Job> deleteJobResp = jobClient.delete("default", "foo1", null);
assertTrue(deleteJobResp.isSuccess());
assertEquals(status, deleteJobResp.getStatus());
assertNull(deleteJobResp.getObject());
verify(1, deleteRequestedFor(urlPathEqualTo("/apis/batch/v1/namespaces/default/jobs/foo1")));
}
@Test
public void getNs() throws ApiException {
Configuration.setDefaultApiClient(apiClient);
V1Namespace ns1 = new V1Namespace().metadata(new V1ObjectMeta().name("name"));
stubFor(
get(urlEqualTo("/api/v1/namespaces/name"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withBody(apiClient.getJSON().serialize(ns1))));
CoreV1Api api = new CoreV1Api();
V1Namespace ns2 = api.readNamespace("name", null, null, null);
assertEquals(ns1, ns2);
}
}
DOClever Mock Server ★★
DOClever 集成了 Mock.js干毅,因此自身就是一個(gè) Mock Server,當(dāng)把接口的開發(fā)狀態(tài)設(shè)置成已完成烈炭,本地 Mock 便會(huì)自動(dòng)請(qǐng)求真實(shí)接口數(shù)據(jù)溶锭,否則返回事先定義好的 Mock 數(shù)據(jù),適合 Web 前后端同學(xué)進(jìn)行開發(fā)自測(cè)符隙。
Mock 總結(jié)
以上趴捅,就是關(guān)于 Mock 技術(shù)以及框架及使用的簡單介紹, 更多詳細(xì)用法還需要參考相應(yīng)的文檔或源碼。
關(guān)于 Mock 服務(wù)框架的選擇:
- 首先要基于團(tuán)隊(duì)的技術(shù)棧來選擇霹疫,這決定了完成服務(wù)"替身"的速度
- 其次拱绑,Mock 要方便快速修改和維護(hù),并能馬上發(fā)揮作用
關(guān)于 Mock 服務(wù)的設(shè)計(jì):
- 首先要簡單
- 其次丽蝎,處理速度比完美的 Mock 服務(wù)更重要
- 最后猎拨,Mock 服務(wù)要能輕量化啟動(dòng),并能容易銷毀屠阻。
代碼覆蓋率
代碼覆蓋率是指红省,至少被執(zhí)行了一次的條目數(shù)占整個(gè)條目數(shù)的百分比,常被用來衡量測(cè)試的充分性和完整性国觉。
常用的三種代碼覆蓋率指標(biāo):
- 行覆蓋率: 又稱語句覆蓋率吧恃,指已經(jīng)被執(zhí)行到的語句占總可執(zhí)行語句的百分比。
- 判定覆蓋: 又稱分支覆蓋麻诀,度量程序中每一個(gè)判定的分支是否都被測(cè)試到了
- 條件覆蓋: 判定中的每個(gè)條件的可能取值至少滿足一次痕寓,度量判定中的每個(gè)條件的結(jié)果 TRUE 和 FALSE 是否都被測(cè)試到了。
代碼覆蓋率的價(jià)值
- 根本目的: 找出潛在的遺漏測(cè)試用例蝇闭,并有針對(duì)性的進(jìn)行補(bǔ)充
- 識(shí)別出由于需求變更等原因造成的不可達(dá)的廢棄代碼
代碼覆蓋率的局限性
- 高的代碼覆蓋率不一定能保證軟件的質(zhì)量呻率,但是低的代碼覆蓋率一定不能能保證軟件的質(zhì)量。如“未考慮某些輸入”以及“未處理某些情況”形成的缺陷呻引。
- 從技術(shù)實(shí)現(xiàn)上講礼仗,單元測(cè)試可以最大化地利用打樁技術(shù)來提高覆蓋率。
- 但在后期,需要付出越來越大的代價(jià)藐守,因?yàn)樾枰罅康臉洞a挪丢、Mock 代碼和全局變量的配合來控制執(zhí)行路徑。
代碼覆蓋率工具
IDEA 覆蓋率工具
執(zhí)行 xxxTest with Coverage
會(huì)進(jìn)行覆蓋率統(tǒng)計(jì)
JaCoCo
JaCoCo 是一款 Java 代碼的主流開源覆蓋率工具卢厂,可以很方便地嵌入到 Maven 中乾蓬,并且和很多主流的持續(xù)集成工具如 Jekins 等以及代碼靜態(tài)檢查工具,都有很好的集成慎恒。在上面 IDEA 覆蓋率配置中選擇 Coverage Runner 為 JaCoCo任内,并導(dǎo)入 pom 依賴后,再運(yùn)行測(cè)試即可得到如下測(cè)試覆蓋率報(bào)告:
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
</dependency>
其它
性能測(cè)試和分析
在前面的筋斗云開發(fā)過程中融柬,遇到過幾次反饋接口響應(yīng)較慢的問題死嗦,因此,針對(duì)這種問題粒氧,在最近筋斗云開發(fā)中越除,開始嘗試學(xué)習(xí)借助一些性能測(cè)試、分析工具來分析代碼具體的執(zhí)行性能外盯,下面給出一些自己的探索:
性能測(cè)試 - JMH
JMH(Java Microbenchmark Harness) 是用于代碼微基準(zhǔn)測(cè)試的工具套件摘盆,由 Oracle 內(nèi)部實(shí)現(xiàn) JIT 的大牛們編寫,主要是基于方法層面的基準(zhǔn)測(cè)試饱苟,精度可以達(dá)到納秒級(jí)孩擂。當(dāng)定位到熱點(diǎn)方法,希望進(jìn)一步優(yōu)化方法性能的時(shí)候箱熬,可以使用 JMH 對(duì)優(yōu)化的結(jié)果進(jìn)行量化的分析类垦。
下面以 Java 中常見的字符串拼接來進(jìn)行對(duì)比 +
和 StringBuilder
的性能測(cè)試對(duì)比:
package jvm;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 3, time = 1)
@Threads(2)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringConnectTest {
@Param(value = {"10", "20"})
private int length;
@Benchmark
public void testStringAdd(Blackhole blackhole) {
String a = "";
for (int i = 0; i < length; i++) {
a += i;
}
blackhole.consume(a);
}
@Benchmark
public void testStringBuilderAdd(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(i);
}
blackhole.consume(sb.toString());
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConnectTest.class.getSimpleName())
.result("result.json")
.resultFormat(ResultFormatType.JSON).build();
new Runner(opt).run();
}
}
測(cè)試結(jié)果:
- 運(yùn)行中:
- 運(yùn)行結(jié)束:
更多性能測(cè)試?yán)涌梢?OpenJDK 官方示例
性能分析 - JProfiler
類似于 Go 中 pprof,可以分析 CPU城须、內(nèi)存等性能蚤认。
使用步驟:
- 安裝 IDEA JProfiler Plugin
- 到官網(wǎng)下載安裝相應(yīng)平臺(tái)的可執(zhí)行程序
- 檢查 IDEA JProfiler 配置
- 以 JProfiler 啟動(dòng)程序
- 執(zhí)行請(qǐng)求,選擇 CPU View 觀察代碼執(zhí)行時(shí)間
系統(tǒng)性能分析
目前這只在應(yīng)用層面代碼上進(jìn)行了一些性能分析和調(diào)優(yōu)的探索糕伐,隨著項(xiàng)目的深入和學(xué)習(xí)砰琢,未來要繼續(xù)深入系統(tǒng)層面的性能優(yōu)化:
- CPU 性能
- CPU 使用率、僵尸進(jìn)程赤炒、CPU 瓶頸...
- 內(nèi)存性能
- 內(nèi)存分配氯析、內(nèi)存泄露亏较、Buffer莺褒、Cache、Swap...
- 網(wǎng)絡(luò)性能
- TCP雪情、HTTP遵岩、RPC、網(wǎng)絡(luò)延遲分析...
- I/O 性能
- 磁盤IO、SQL 查詢尘执、Redis舍哄、數(shù)據(jù)庫...
Code Review
廣義的單元測(cè)試,是這三部分的有機(jī)組合:
- Code Review
- 靜態(tài)代碼掃描
- 單元測(cè)試用例編寫
Code Review 在單元測(cè)試中也起到了很重要的作用誊锭。自從參與項(xiàng)目以來表悬,自己的代碼也被成哥和爽哥 review 過幾次,不僅避免了一些小問題導(dǎo)致的 bug丧靡,而且從中也學(xué)習(xí)到了一些內(nèi)容蟆沫。
同時(shí),也看到組內(nèi)同事棒哥之前也進(jìn)行過 Code Review 的經(jīng)驗(yàn)分享温治,包括 MR 的規(guī)范以及部分代碼示例饭庞,其中也提到了 Code Review 來作為單元測(cè)試的前提。
另外熬荆,Google 也分享了 Code Review 實(shí)踐 Google's Engineering Practices documentation - How to do a code review舟山,或許可以借鑒學(xué)習(xí)。
總之卤恳,Code Review 不僅能規(guī)避一些問題累盗,同時(shí)還可以互相學(xué)習(xí),形成代碼規(guī)范纬黎,共同努力提高代碼質(zhì)量幅骄。
總結(jié):實(shí)際項(xiàng)目中如何開展單元測(cè)試
- 不是所有的代碼都要進(jìn)行單元測(cè)試,通常只有底層模塊或者核心模塊的測(cè)試中才會(huì)采用單元測(cè)試
- 確定單元測(cè)試框架的選型本今,這和開發(fā)語言直接相關(guān)
- 引入計(jì)算代碼覆蓋率的工具拆座,衡量單元測(cè)試的代碼覆蓋率
- 單元測(cè)試執(zhí)行、代碼覆蓋率統(tǒng)計(jì)和持續(xù)集成流水線做集成冠息,確保每次代碼遞交挪凑,都會(huì)自動(dòng)觸發(fā)單元測(cè)試,并在單元測(cè)試執(zhí)行過程中自動(dòng)統(tǒng)計(jì)代碼覆蓋率逛艰,最后 以“單元測(cè)試通過率”和“代碼覆蓋率”為標(biāo)準(zhǔn) 來決定本次代碼遞交是否能夠被接受躏碳。
參考:
單元測(cè)試
- 單元測(cè)試維基百科
- 單元測(cè)試到底是什么?應(yīng)該怎么做散怖?
- 從頭到腳說單測(cè)——談?dòng)行У膯卧獪y(cè)試
- 什么是單元測(cè)試菇绵?如何做好單元測(cè)試?
- 你真的懂測(cè)試覆蓋率嗎镇眷?
- 在 idea 中使用 JaCoCo 插件統(tǒng)計(jì)單元測(cè)試覆蓋率
Mock
Moco
MockMVC
- Spring Boot 構(gòu)建 RESTful API 與單元測(cè)試
- 微服務(wù)單元測(cè)試 Mock 使用與詳解
- 利用 Junit + MockMvc + Mockito 對(duì) Http 請(qǐng)求進(jìn)行單元測(cè)試
Mockito & PowerMock
Wiremock
- WireMock Getting Started
- WireMock and Spring MVC Mocks
- spring-cloud/spring-cloud-contract
- kubernetes-client