vivo 基于 JaCoCo 的測(cè)試覆蓋率設(shè)計(jì)與實(shí)踐
作者|vivo 互聯(lián)網(wǎng)服務(wù)器團(tuán)隊(duì)- Xu Shen
本文主要介紹vivo內(nèi)部研發(fā)平臺(tái)使用JaCoCo實(shí)現(xiàn)測(cè)試覆蓋率的實(shí)踐,包括JaCoCo原理介紹以及在實(shí)踐過(guò)程中遇到的新增代碼覆蓋率統(tǒng)計(jì)問(wèn)題和頻繁發(fā)布導(dǎo)致覆蓋率丟失問(wèn)題的解決辦法。
一、為什么需要測(cè)試覆蓋率
1.1 在日常研發(fā)過(guò)程中,經(jīng)常發(fā)現(xiàn)一些問(wèn)題
- 測(cè)試案例的設(shè)計(jì)憑經(jīng)驗(yàn),當(dāng)研發(fā)一個(gè)新功能時(shí),經(jīng)常對(duì)測(cè)試場(chǎng)景估計(jì)不足,到上線后發(fā)現(xiàn)bug;
- 開(kāi)發(fā)經(jīng)常做一些需求之外的代碼變更(代碼小范圍內(nèi)重構(gòu)或在開(kāi)發(fā)過(guò)程中發(fā)現(xiàn)小缺陷隨手改掉),導(dǎo)致測(cè)試任務(wù)無(wú)法測(cè)試到對(duì)應(yīng)的場(chǎng)景,引起線上問(wèn)題;
- 對(duì)測(cè)試效果無(wú)法量化考核,導(dǎo)致測(cè)試工作的質(zhì)量無(wú)法進(jìn)一步提升。
1.2. 有沒(méi)有技術(shù)手段能夠盡可能的避免上面的問(wèn)題呢?
在業(yè)內(nèi)已經(jīng)在普遍使用代碼覆蓋率來(lái)提升測(cè)試質(zhì)量,那什么是代碼覆蓋率?
代碼覆蓋率是軟件測(cè)試中的一種度量,描述程序中源代碼被測(cè)試的比例和程度,所得比例稱(chēng)為代碼覆蓋率 。
代碼覆蓋率指標(biāo)通常包含下面幾類(lèi):
- 函數(shù)/方法覆蓋率:函數(shù)/方法中有多少被調(diào)用到
- 分支覆蓋率:有多少控制結(jié)構(gòu)的分支(例如if語(yǔ)句)被執(zhí)行
- 條件覆蓋率:有多少布爾子表達(dá)式被測(cè)試為真值和假值
- 行覆蓋率:有多少行的源代碼被測(cè)試過(guò)
1.3 在使用測(cè)試覆蓋率的過(guò)程中,經(jīng)常發(fā)現(xiàn)的場(chǎng)景
- if/else語(yǔ)句中,if{}內(nèi)的代碼被覆蓋到,else{}內(nèi)的代碼沒(méi)有被覆蓋到,可以得出部分分支場(chǎng)景沒(méi)有測(cè)試到;
- try/catch語(yǔ)句中,try{}內(nèi)的代碼被覆蓋到,catch{}內(nèi)的代碼沒(méi)有被覆蓋到,可以得出異常場(chǎng)景沒(méi)有測(cè)試到;
- if (條件1 || 條件2 || 條件3)語(yǔ)句中,條件1被覆蓋到,條件2和條件3沒(méi)有被覆蓋到,可以得出部分條件場(chǎng)景沒(méi)有測(cè)試到;
測(cè)試人員對(duì)代碼覆蓋率的指標(biāo)正確使用,能有效提升測(cè)試的質(zhì)量,進(jìn)而提升版本的上線質(zhì)量。
二、JaCoCo在測(cè)試覆蓋率場(chǎng)景中的使用
2.1 JaCoCo介紹
當(dāng)前主流的代碼覆蓋率工具:
- C/C++→Gcov ,Java→JaCoCo,JavaScript→ Istanbul。
- 考慮到服務(wù)器端主要是Java語(yǔ)言,所以CICD平臺(tái)優(yōu)先使用JaCoCo來(lái)支持 Java 語(yǔ)言的代碼覆蓋率統(tǒng)計(jì)能力。
- 通過(guò)JaCoCo官網(wǎng),我們可以看到JaCoCo的使命是為Java VM 的環(huán)境中的代碼覆蓋分析提供標(biāo)準(zhǔn)技術(shù)。重點(diǎn)是提供一個(gè)輕量級(jí)、靈活且有據(jù)可查的庫(kù),用于與各種構(gòu)建和開(kāi)發(fā)工具集成。
2.2 JaCoCo優(yōu)點(diǎn)
- JaCoCo支持指令(C0)、分支(C1)、行、方法、類(lèi)和圈復(fù)雜度等多維度的覆蓋分析;
- 基于 Java 字節(jié)碼,也可以在沒(méi)有源文件的情況下工作;
- 性能良好,運(yùn)行時(shí)開(kāi)銷(xiāo)很小,尤其是對(duì)于大型項(xiàng)目;
- 比較完整的API,很方便與其他工具進(jìn)行集成;
- 遠(yuǎn)程協(xié)議和 JMX 控制可在任何時(shí)間點(diǎn)從代理請(qǐng)求執(zhí)行數(shù)據(jù)下載。
2.3 JaCoCo原理
主要來(lái)自于JaCoCo官方網(wǎng)站
JaCoCo支持幾種不同的方法來(lái)收集覆蓋信息,對(duì)于每種方法,由不同技術(shù)實(shí)現(xiàn)的,下圖橙色路徑部分是JaCoCo 推薦使用的方式,即通過(guò)On-The-Fly方式收集覆蓋率信息:
通過(guò)上圖我們知道,JaCoCo 是通過(guò)對(duì)Java字節(jié)碼(Byte Code)插入探針的方式來(lái)收集覆蓋率信息的,探針是可以插入現(xiàn)有指令之間的附加指令。它們不會(huì)改變方法的行為,但會(huì)記錄它們已被執(zhí)行的事實(shí)。
下面以一段簡(jiǎn)單的 程序?yàn)槔M(jìn)行說(shuō)明:
這段代碼經(jīng)過(guò)Java編譯以后轉(zhuǎn)化為以下字節(jié)碼:
因?yàn)镴ava 字節(jié)碼指令的線性序列,控制流是通過(guò)條件或無(wú)條件指令實(shí)現(xiàn)跳轉(zhuǎn)的,跳轉(zhuǎn)目標(biāo)在技術(shù)上是相對(duì)于目標(biāo)指令的偏移量。這個(gè)跟大學(xué)學(xué)習(xí)的匯編指令的跳轉(zhuǎn)方式類(lèi)似,為了更好的可讀性,使用符號(hào)標(biāo)簽 (L1,L2 ) 代替實(shí)際的指令地址。
上圖中橙色的部分為插入的探針,理論上我們可以在控制流圖的每個(gè)邊緣插入一個(gè)探針,由于探針實(shí)現(xiàn)本身需要一些字節(jié)碼指令,這將會(huì)使類(lèi)文件的大小增加數(shù)倍;幸運(yùn)的是,這不是必需的,實(shí)際上我們只需要根據(jù)方法的控制流為每個(gè)方法插入幾個(gè)探針。例如,沒(méi)有任何分支的方法只需要一個(gè)探針。
如果已經(jīng)執(zhí)行了探測(cè),我們就知道相應(yīng)的邊已經(jīng)被訪問(wèn)過(guò)。從這條邊我們可以得出結(jié)論到其他前面的節(jié)點(diǎn)和邊:
如果一條邊被訪問(wèn)過(guò),我們就知道這條邊的源節(jié)點(diǎn)已經(jīng)被執(zhí)行了;
如果一個(gè)節(jié)點(diǎn)已經(jīng)被執(zhí)行并且該節(jié)點(diǎn)是只有一條邊的目標(biāo),我們知道這條邊已經(jīng)被訪問(wèn)過(guò)。
如果我們?cè)谡_的位置有探針,遞歸地應(yīng)用這些規(guī)則可以確定方法的所有指令的執(zhí)行狀態(tài),探針只是需要在控制流邊緣插入的一小段附加指令。
三、CICD平臺(tái)關(guān)于測(cè)試覆蓋率的解決方案
通過(guò)上面對(duì)JaCoCo原理的介紹,結(jié)合我們公司內(nèi)部的研發(fā)流程,在CICD平臺(tái)對(duì)代碼覆蓋率功能的設(shè)計(jì)如下:
從上面 CICD 平臺(tái)對(duì)測(cè)試覆蓋率的設(shè)計(jì)圖,大概可以看出來(lái),整個(gè)過(guò)程包含三個(gè)階段
3.1 測(cè)試前
測(cè)試前由測(cè)試人員(開(kāi)發(fā)人員/運(yùn)維人員)在流水線上開(kāi)啟測(cè)試覆蓋率功能,在流水線執(zhí)行發(fā)布時(shí),會(huì)在測(cè)試環(huán)境上下載JaCoCo Agent包,并在Java進(jìn)程啟動(dòng)時(shí)配置JavaAgent參數(shù);
在進(jìn)程啟動(dòng)過(guò)程或啟動(dòng)之后,有class文件被加載時(shí)被Agent攔截,對(duì)class文件進(jìn)行插樁處理,在必要的路徑下插入探針(插入探針的原理在上一節(jié)已經(jīng)介紹)。
3.2 測(cè)試中
在測(cè)試過(guò)程中,測(cè)試人員在測(cè)試環(huán)境執(zhí)行測(cè)試案例(手動(dòng)執(zhí)行或自動(dòng)化腳本),被調(diào)用到的代碼會(huì)被探針記錄下來(lái),探針數(shù)據(jù)保存在Java進(jìn)程的內(nèi)存中。
3.3 測(cè)試后
測(cè)試人員可以多次發(fā)布測(cè)試環(huán)境,針對(duì)同一個(gè)分支的代碼,可以合并多次測(cè)試的結(jié)果數(shù)據(jù),形成全量的覆蓋率數(shù)據(jù);
在測(cè)試結(jié)束后,CICD平臺(tái)通過(guò)JaCoCo的API,手動(dòng)/自動(dòng)下載(dump)覆蓋率數(shù)據(jù),合并(merge)歷史覆蓋率數(shù)據(jù),生成測(cè)試覆蓋率報(bào)告;
測(cè)試人員根據(jù)測(cè)試覆蓋率報(bào)告的結(jié)果,查看測(cè)試遺漏的場(chǎng)景,進(jìn)行補(bǔ)充測(cè)試,事后總結(jié)遺漏的原因,提高測(cè)試效率。
四、在實(shí)踐過(guò)程中遇到的問(wèn)題及解決辦法
測(cè)試覆蓋率在上線運(yùn)行一段時(shí)間后,在實(shí)踐過(guò)程中發(fā)現(xiàn)了一些問(wèn)題,總結(jié)為以下幾點(diǎn):
4.1 在不同機(jī)器編譯會(huì)導(dǎo)致classid不一致的問(wèn)題
在實(shí)踐過(guò)程中,經(jīng)常遇到這樣一個(gè)問(wèn)題,用戶(hù)反饋并確認(rèn)案例已經(jīng)正常執(zhí)行,但是生成的報(bào)告顯示未覆蓋,經(jīng)過(guò)調(diào)查發(fā)現(xiàn)在測(cè)試環(huán)境中的class和生成報(bào)告時(shí)的class不一致導(dǎo)致的。
在 JaCoCo內(nèi)部,覆蓋率數(shù)據(jù)是以classid作為key來(lái)存儲(chǔ)的,classid是根據(jù)class的字節(jié)碼hash算法得出來(lái)的,看JaCoCo源碼中關(guān)于classid的算法如下:
出現(xiàn)不一致的情況包括:
- 發(fā)布時(shí)編譯的機(jī)器和生成報(bào)告的機(jī)器環(huán)境上有差異,比如操作系統(tǒng)版本、JDK版本等,導(dǎo)致編譯的class不一致;
- 發(fā)布時(shí)編譯的代碼版本與生成報(bào)告時(shí)的代碼版本有差異,導(dǎo)致編譯的class不一致。
要解決上面環(huán)境的問(wèn)題,需要保持在測(cè)試覆蓋率過(guò)程中編譯的機(jī)器環(huán)境保持一致,或者做到只編譯一次,使用同一份class文件,考慮到存儲(chǔ)空間的問(wèn)題,vivo采用保持環(huán)境一致的辦法來(lái)解決。
對(duì)于第二種情況,常見(jiàn)于采用敏捷研發(fā)的團(tuán)隊(duì),在一個(gè)版本中按功能點(diǎn)轉(zhuǎn)測(cè),經(jīng)常導(dǎo)致測(cè)試在測(cè)試過(guò)程中,源代碼已經(jīng)發(fā)生了修改,生成報(bào)告時(shí)代碼版本和發(fā)布時(shí)的代碼版本已經(jīng)不一致,這種情況比較復(fù)雜,我們?cè)谙旅鏁?huì)介紹。
4.2 在研發(fā)過(guò)程中更加關(guān)注增量代碼的覆蓋率
在我們?nèi)粘5难邪l(fā)活動(dòng)中,對(duì)于全量代碼更多使用自動(dòng)化腳本來(lái)回歸,而新研發(fā)的功能主要表現(xiàn)為增量代碼,對(duì)于增量代碼的覆蓋率情況更加關(guān)注, JaCoCo本身不支持增量代碼的覆蓋率。
對(duì)于這個(gè)問(wèn)題網(wǎng)上也有不少解決方案,基本都是基于git的版本差異,在生成報(bào)告時(shí)過(guò)濾掉沒(méi)有差異的類(lèi),形成兩份覆蓋率報(bào)告,一份是全量代碼覆蓋率報(bào)告,一份是增量代碼覆蓋率報(bào)告,而我們更希望在一份覆蓋率報(bào)告中呈現(xiàn)增量代碼和全量代碼的覆蓋情況,結(jié)合代碼在全量報(bào)告中的覆蓋路徑分析遺漏的場(chǎng)景,同時(shí)能在報(bào)告中標(biāo)注增量代碼和增量代碼的覆蓋情況,期望的效果如下圖所示:
為了達(dá)到上述效果,需要幾個(gè)改造步驟:
- 計(jì)算出當(dāng)前代碼分支的變動(dòng)情況,需要精確到代碼行
- 改造JaCoCo計(jì)算邏輯,針對(duì)增量代碼單獨(dú)統(tǒng)計(jì)覆蓋率指標(biāo)值
- 改造JaCoCo報(bào)告格式,在報(bào)告中兼容全量代碼和增量代碼的覆蓋情況
對(duì)于計(jì)算代碼分支的變動(dòng)情況,放棄 GitLab 提供的代碼比對(duì)功能來(lái)獲取不同版本之前的差異信息,如果版本之間差異太多的話,經(jīng)常發(fā)生GitLab 的API接口調(diào)用超時(shí);
并且GitLab 的比對(duì)功能無(wú)法滿足定制場(chǎng)景,比如一行代碼僅僅因?yàn)楦袷交蛔R(shí)別為變更代碼等等,采用借助Linux自帶的diff命令,實(shí)現(xiàn)代碼差異比對(duì)的能力:
對(duì)于改造 JaCoCo計(jì)算邏輯,增加針對(duì)增量代碼的覆蓋率指標(biāo)統(tǒng)計(jì),在CoverageNodeImpl類(lèi)中增加新的Counter,用于統(tǒng)計(jì)新增類(lèi)、方法、行、指令覆蓋率指標(biāo);在SourceNodeImple類(lèi)中increment方法中增加新增代碼行的統(tǒng)計(jì)邏輯。
4.3 重談關(guān)于classid的問(wèn)題
在上面已經(jīng)談到關(guān)于classid的問(wèn)題,如果是環(huán)境問(wèn)題是比較好解決,但是現(xiàn)在互聯(lián)網(wǎng)團(tuán)隊(duì)基本都使用敏捷模式,基本不太可能等開(kāi)發(fā)工作全部完成再轉(zhuǎn)測(cè),這樣必然會(huì)導(dǎo)致最新的覆蓋率報(bào)告,會(huì)出現(xiàn)以類(lèi)為單元的覆蓋率數(shù)據(jù)丟失,需要測(cè)試人員來(lái)回重復(fù)的執(zhí)行測(cè)試案例,否則測(cè)試覆蓋率數(shù)據(jù)不會(huì)很好看。
既然知道問(wèn)題所在,那有沒(méi)有辦法解決呢?是不是可以直接找到以前的classid,把以前的classid對(duì)應(yīng)的探針數(shù)據(jù)復(fù)制到當(dāng)前的classid下就可以?當(dāng)然是不行的,因?yàn)樵创a發(fā)生變動(dòng),導(dǎo)致探針的數(shù)量發(fā)生變化,會(huì)出現(xiàn)下面的情況:
或者這樣
出現(xiàn)這樣的情況,會(huì)無(wú)法判斷具體哪些探針是新增的或者刪除的;即使出現(xiàn)前后探針一致的情況,也有可能因?yàn)榇a修改,探針位置發(fā)生變化:
那么這個(gè)問(wèn)題是否就無(wú)解了呢?這里給出一個(gè)大概思路,現(xiàn)在的覆蓋率數(shù)據(jù)是以類(lèi)為單位存儲(chǔ)的,我們可以修改存儲(chǔ)的粒度,細(xì)化到方法級(jí)別,這樣可以保留一個(gè)類(lèi)的大部分探針數(shù)據(jù),這樣如果只是修改一個(gè)方法的話,那么其他方法的測(cè)試數(shù)據(jù)可以繼續(xù)保留,只需要重新測(cè)試這個(gè)方法就行,這樣可以有效的降低測(cè)試人員對(duì)整個(gè)類(lèi)的所有方案重復(fù)測(cè)試的情況。
五、總結(jié)
對(duì)于測(cè)試覆蓋率功能,有沒(méi)有給測(cè)試的質(zhì)量帶來(lái)提升,答案是顯而易見(jiàn)的。
當(dāng)然也因?yàn)樯厦嫣岬降膯?wèn)題,給測(cè)試人員帶了些麻煩,為了提升測(cè)試覆蓋率數(shù)據(jù),導(dǎo)致測(cè)試人員對(duì)同一個(gè)功能重復(fù)多次測(cè)試;同時(shí)也給測(cè)試人員帶來(lái)了好處,很多測(cè)試人員在面對(duì)測(cè)試覆蓋率指標(biāo)嚴(yán)格要求下,被迫去看代碼的實(shí)現(xiàn)邏輯,提升了自己業(yè)務(wù)水平和閱讀代碼的水平,甚至出現(xiàn)測(cè)試人員和開(kāi)發(fā)人員當(dāng)面對(duì)質(zhì),關(guān)于代碼邏輯是否合理的場(chǎng)景。
最后,測(cè)試覆蓋率不是衡量測(cè)試質(zhì)量的唯一標(biāo)準(zhǔn),要合理利用測(cè)試覆蓋率來(lái)提升測(cè)試質(zhì)量。