從 0 到 0.1 入門單元測(cè)試

單元測(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ū)

  1. 輸入:只有被測(cè)試函數(shù)的輸入?yún)?shù)是“輸入數(shù)據(jù)”
  2. 輸出:只有函數(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)。
驅(qū)動(dòng)代碼
樁代碼-被測(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光戈。

樁代碼原則
  1. 具有與原函數(shù)完全相同的原形就轧,僅僅是內(nèi)部實(shí)現(xiàn)不同
  2. 用于隔離和補(bǔ)齊的樁函數(shù),只需保持原函數(shù)聲明田度,加一個(gè)空實(shí)現(xiàn),目的是通過編譯鏈接
  3. 控制功能的樁函數(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):

  1. 測(cè)試數(shù)據(jù)的準(zhǔn)備和清理
  2. 分布式系統(tǒng)的依賴
  3. 測(cè)試用例高度仿真

解決方案:

  1. Mock
  2. “回放”技術(shù)(記錄實(shí)際用戶在生產(chǎn)環(huán)境的操作异希,然后在測(cè)試環(huán)境中回放)
    1. 攔截:
      1. SLB 統(tǒng)一做攔截和復(fù)制轉(zhuǎn)發(fā)處理健盒;主路徑影響路由,容易故障
      2. 集群擴(kuò)容一臺(tái)軟交換服務(wù)器称簿,負(fù)責(zé)復(fù)制和轉(zhuǎn)發(fā)用戶請(qǐng)求扣癣;
    2. 回放

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è)試授药。

分布式系統(tǒng)依賴

還有的會(huì)依賴數(shù)據(jù)庫士嚎、消息中間件等:

測(cè)試過程中,被測(cè)對(duì)象的外部依賴情況展示

Mock 基本概念介紹

Mock 測(cè)試就是在測(cè)試過程中呜魄,對(duì)于某些不容易構(gòu)造或者不容易獲取的對(duì)象,用一個(gè)虛擬的對(duì)象來創(chuàng)建以便測(cè)試的測(cè)試方法莱衩。

好處

  1. 團(tuán)隊(duì)并行工作
    團(tuán)隊(duì)間不需互相等待對(duì)方進(jìn)度爵嗅,只需約定好相互之間的數(shù)據(jù)規(guī)范(接口文檔),即可使用 mock 構(gòu)建出可用接口膳殷,然后盡快進(jìn)行開發(fā)和自測(cè)操骡,提前發(fā)現(xiàn)缺陷

  2. 測(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è)試。

  3. 測(cè)試覆蓋率
    若一個(gè)接口在不同的狀態(tài)下要返回不同的值辱匿,常見做法是復(fù)現(xiàn)這種狀態(tài)然后再去請(qǐng)求接口键痛,而這種方法很可能因操作時(shí)機(jī)或方式不當(dāng)導(dǎo)致失敗,甚至污染后端存儲(chǔ)如數(shù)據(jù)庫等, 但用 mock 則不用擔(dān)心

  4. 隔離系統(tǒng)
    使用某些接口時(shí)匾七,為避免系統(tǒng)數(shù)據(jù)庫被污染絮短,可以將接口調(diào)整為 Mock 模式,以保證數(shù)據(jù)庫純凈昨忆。

  5. 方便演示

Mock 框架介紹

Mock 技術(shù)主要的應(yīng)用場(chǎng)景可以分為兩類:

  1. 基于對(duì)象和類的 Mock

    1. Mockito & PowerMock
  2. 基于微服務(wù)的 Mock

    1. 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, 在包括 openstack4jkubernetes-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)行簡單舉例:

  1. 使用 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

  1. 在項(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 Server

Mock 總結(jié)

以上趴捅,就是關(guān)于 Mock 技術(shù)以及框架及使用的簡單介紹, 更多詳細(xì)用法還需要參考相應(yīng)的文檔或源碼。

關(guān)于 Mock 服務(wù)框架的選擇:

  1. 首先要基于團(tuán)隊(duì)的技術(shù)棧來選擇霹疫,這決定了完成服務(wù)"替身"的速度
  2. 其次拱绑,Mock 要方便快速修改和維護(hù),并能馬上發(fā)揮作用

關(guān)于 Mock 服務(wù)的設(shè)計(jì):

  1. 首先要簡單
  2. 其次丽蝎,處理速度比完美的 Mock 服務(wù)更重要
  3. 最后猎拨,Mock 服務(wù)要能輕量化啟動(dòng),并能容易銷毀屠阻。

代碼覆蓋率

代碼覆蓋率是指红省,至少被執(zhí)行了一次的條目數(shù)占整個(gè)條目數(shù)的百分比,常被用來衡量測(cè)試的充分性和完整性国觉。

常用的三種代碼覆蓋率指標(biāo):

  1. 行覆蓋率: 又稱語句覆蓋率吧恃,指已經(jīng)被執(zhí)行到的語句占總可執(zhí)行語句的百分比。
  2. 判定覆蓋: 又稱分支覆蓋麻诀,度量程序中每一個(gè)判定的分支是否都被測(cè)試到了
  3. 條件覆蓋: 判定中的每個(gè)條件的可能取值至少滿足一次痕寓,度量判定中的每個(gè)條件的結(jié)果 TRUE 和 FALSE 是否都被測(cè)試到了。

代碼覆蓋率的價(jià)值

  • 根本目的: 找出潛在的遺漏測(cè)試用例蝇闭,并有針對(duì)性的進(jìn)行補(bǔ)充
  • 識(shí)別出由于需求變更等原因造成的不可達(dá)的廢棄代碼

代碼覆蓋率的局限性

  1. 高的代碼覆蓋率不一定能保證軟件的質(zhì)量呻率,但是低的代碼覆蓋率一定不能能保證軟件的質(zhì)量。如“未考慮某些輸入”以及“未處理某些情況”形成的缺陷呻引。
  2. 從技術(shù)實(shí)現(xiàn)上講礼仗,單元測(cè)試可以最大化地利用打樁技術(shù)來提高覆蓋率。
  3. 但在后期,需要付出越來越大的代價(jià)藐守,因?yàn)樾枰罅康臉洞a挪丢、Mock 代碼和全局變量的配合來控制執(zhí)行路徑。

代碼覆蓋率工具

IDEA 覆蓋率工具

執(zhí)行 xxxTest with Coverage 會(huì)進(jìn)行覆蓋率統(tǒng)計(jì)

IDEA 覆蓋率工具
IDEA 覆蓋率結(jié)果
IDEA 覆蓋率配置

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>
JaCoCo 覆蓋率統(tǒng)計(jì)

其它

性能測(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)行中:
JMH運(yùn)行中
  • 運(yùn)行結(jié)束:
JMH運(yùn)行結(jié)束

更多性能測(cè)試?yán)涌梢?OpenJDK 官方示例

性能分析 - JProfiler

類似于 Go 中 pprof,可以分析 CPU城须、內(nèi)存等性能蚤认。

使用步驟:

  1. 安裝 IDEA JProfiler Plugin
jprofile-plugin
  1. 官網(wǎng)下載安裝相應(yīng)平臺(tái)的可執(zhí)行程序
jprofile-exe
  1. 檢查 IDEA JProfiler 配置
idea-jprofile
  1. 以 JProfiler 啟動(dòng)程序
jprofile-launch
  1. 執(zhí)行請(qǐng)求,選擇 CPU View 觀察代碼執(zhí)行時(shí)間
cpu-view

系統(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è)試

  1. 不是所有的代碼都要進(jìn)行單元測(cè)試,通常只有底層模塊或者核心模塊的測(cè)試中才會(huì)采用單元測(cè)試
  2. 確定單元測(cè)試框架的選型本今,這和開發(fā)語言直接相關(guān)
  3. 引入計(jì)算代碼覆蓋率的工具拆座,衡量單元測(cè)試的代碼覆蓋率
  4. 單元測(cè)試執(zhí)行、代碼覆蓋率統(tǒng)計(jì)和持續(xù)集成流水線做集成冠息,確保每次代碼遞交挪凑,都會(huì)自動(dòng)觸發(fā)單元測(cè)試,并在單元測(cè)試執(zhí)行過程中自動(dòng)統(tǒng)計(jì)代碼覆蓋率逛艰,最后 以“單元測(cè)試通過率”和“代碼覆蓋率”為標(biāo)準(zhǔn) 來決定本次代碼遞交是否能夠被接受躏碳。

參考:

單元測(cè)試
Mock
Moco
MockMVC
Mockito & PowerMock
Wiremock
Code Review
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市欠动,隨后出現(xiàn)的幾起案子永乌,更是在濱河造成了極大的恐慌惑申,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翅雏,死亡現(xiàn)場(chǎng)離奇詭異圈驼,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)望几,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門绩脆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人橄抹,你說我怎么就攤上這事衙伶。” “怎么了害碾?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵矢劲,是天一觀的道長。 經(jīng)常有香客問我慌随,道長芬沉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任阁猜,我火速辦了婚禮丸逸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘剃袍。我一直安慰自己黄刚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布民效。 她就那樣靜靜地躺著憔维,像睡著了一般。 火紅的嫁衣襯著肌膚如雪畏邢。 梳的紋絲不亂的頭發(fā)上业扒,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音舒萎,去河邊找鬼程储。 笑死,一個(gè)胖子當(dāng)著我的面吹牛臂寝,可吹牛的內(nèi)容都是我干的章鲤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼咆贬,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼败徊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起素征,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤集嵌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后御毅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體根欧,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年端蛆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凤粗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡今豆,死狀恐怖嫌拣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呆躲,我是刑警寧澤异逐,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站插掂,受9級(jí)特大地震影響灰瞻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辅甥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一酝润、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧璃弄,春花似錦要销、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至脐供,卻和暖如春凳鬓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背患民。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工缩举, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人匹颤。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓仅孩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親印蓖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辽慕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353