vivo 基於 JaCoCo 的測試覆蓋率設計與實踐

2022-08-29 12:04:33

作者:vivo 網際網路伺服器團隊- Xu Shen

本文主要介紹vivo內部研發平臺使用JaCoCo實現測試覆蓋率的實踐,包括JaCoCo原理介紹以及在實踐過程中遇到的新增程式碼覆蓋率統計問題和頻繁釋出導致覆蓋率丟失問題的解決辦法。

一、為什麼需要測試覆蓋率

1.1 在日常研發過程中,經常發現一些問題

  • 測試案例的設計憑經驗,當研發一個新功能時,經常對測試場景估計不足,到上線後發現bug;

  • 開發經常做一些需求之外的程式碼變更(程式碼小範圍內重構或在開發過程中發現小缺陷隨手改掉),導致測試任務無法測試到對應的場景,引起線上問題;

  • 對測試效果無法量化考核,導致測試工作的質量無法進一步提升。

1.2. 有沒有技術手段能夠儘可能的避免上面的問題呢?

在業內已經在普遍使用程式碼覆蓋率來提升測試質量,那什麼是程式碼覆蓋率?

程式碼覆蓋率是軟體測試中的一種度量,描述程式中原始碼被測試的比例和程度,所得比例稱為程式碼覆蓋率 

程式碼覆蓋率指標通常包含下面幾類:

  • 函數/方法覆蓋率:函數/方法中有多少被呼叫到

  • 分支覆蓋率:有多少控制結構的分支(例如if語句)被執行

  • 條件覆蓋率:有多少布林子表示式被測試為真值和假值

  • 行覆蓋率:有多少行的原始碼被測試過

1.3 在使用測試覆蓋率的過程中,經常發現的場景

  • if/else語句中,if{}內的程式碼被覆蓋到,else{}內的程式碼沒有被覆蓋到,可以得出部分分支場景沒有測試到;

  • try/catch語句中,try{}內的程式碼被覆蓋到,catch{}內的程式碼沒有被覆蓋到,可以得出異常場景沒有測試到;

  • if (條件1 || 條件2 || 條件3)語句中,條件1被覆蓋到,條件2和條件3沒有被覆蓋到,可以得出部分條件場景沒有測試到;

 測試人員對程式碼覆蓋率的指標正確使用,能有效提升測試的質量,進而提升版本的上線質量。

二、JaCoCo在測試覆蓋率場景中的使用

2.1  JaCoCo介紹

當前主流的程式碼覆蓋率工具:   

C/C++→Gcov ,Java→JaCoCo,JavaScript→ Istanbul。

考慮到伺服器端主要是Java語言,所以CICD平臺優先使用JaCoCo來支援 Java 語言的程式碼覆蓋率統計能力。

通過JaCoCo官網,我們可以看到JaCoCo的使命是為Java VM 的環境中的程式碼覆蓋分析提供標準技術。重點是提供一個輕量級、靈活且有據可查的庫,用於與各種構建和開發工具整合。

2.2 JaCoCo優點

  • JaCoCo支援指令(C0)、分支(C1)、行、方法、類和圈複雜度等多維度的覆蓋分析;

  • 基於 Java 位元組碼,也可以在沒有原始檔的情況下工作;

  • 效能良好,執行時開銷很小,尤其是對於大型專案;

  • 比較完整的API,很方便與其他工具進行整合;

  • 遠端協定和 JMX 控制可在任何時間點從代理請求執行資料下載。

2.3 JaCoCo原理

主要來自於JaCoCo官方網站

JaCoCo支援幾種不同的方法來收集覆蓋資訊,對於每種方法,由不同技術實現的,下圖橙色路徑部分是JaCoCo 推薦使用的方式,即通過On-The-Fly方式收集覆蓋率資訊:

圖片

通過上圖我們知道,JaCoCo 是通過對Java位元組碼(Byte Code)插入探針的方式來收集覆蓋率資訊的,探針是可以插入現有指令之間的附加指令。它們不會改變方法的行為,但會記錄它們已被執行的事實。

下面以一段簡單的  程式為例進行說明:

 

圖片

這段程式碼經過Java編譯以後轉化為以下位元組碼:

圖片

因為Java 位元組碼指令的線性序列,控制流是通過條件或無條件指令實現跳轉的,跳轉目標在技術上是相對於目標指令的偏移量。這個跟大學學習的組合指令的跳轉方式類似,為了更好的可讀性,使用符號標籤 (L1,L2 ) 代替實際的指令地址。

圖片

上圖中橙色的部分為插入的探針,理論上我們可以在控制流圖的每個邊緣插入一個探針,由於探針實現本身需要一些位元組碼指令,這將會使類檔案的大小增加數倍;幸運的是,這不是必需的,實際上我們只需要根據方法的控制流為每個方法插入幾個探針。例如,沒有任何分支的方法只需要一個探針。

如果已經執行了探測,我們就知道相應的邊已經被存取過。從這條邊我們可以得出結論到其他前面的節點和邊:

  • 如果一條邊被存取過,我們就知道這條邊的源節點已經被執行了;

  • 如果一個節點已經被執行並且該節點是隻有一條邊的目標,我們知道這條邊已經被存取過。

 

如果我們在正確的位置有探針,遞迴地應用這些規則可以確定方法的所有指令的執行狀態,探針只是需要在控制流邊緣插入的一小段附加指令。

三、CICD平臺關於測試覆蓋率的解決方案

 

通過上面對JaCoCo原理的介紹,結合我們公司內部的研發流程,在CICD平臺對程式碼覆蓋率功能的設計如下:

圖片

從上面 CICD 平臺對測試覆蓋率的設計圖,大概可以看出來,整個過程包含三個階段

3.1 測試前

測試前由測試人員(開發人員/運維人員)在流水線上開啟測試覆蓋率功能,在流水線執行釋出時,會在測試環境上下載JaCoCo Agent包,並在Java程序啟動時設定JavaAgent引數;

在程序啟動過程或啟動之後,有class檔案被載入時被Agent攔截,對class檔案進行插樁處理,在必要的路徑下插入探針(插入探針的原理在上一節已經介紹)。

3.2 測試中

在測試過程中,測試人員在測試環境執行測試案例(手動執行或自動化指令碼),被呼叫到的程式碼會被探針記錄下來,探針資料儲存在Java程序的記憶體中。

3.3  測試後

測試人員可以多次釋出測試環境,針對同一個分支的程式碼,可以合併多次測試的結果資料,形成全量的覆蓋率資料;

在測試結束後,CICD平臺通過JaCoCo的API,手動/自動下載(dump)覆蓋率資料,合併(merge)歷史覆蓋率資料,生成測試覆蓋率報告;

測試人員根據測試覆蓋率報告的結果,檢視測試遺漏的場景,進行補充測試,事後總結遺漏的原因,提高測試效率。

四、在實踐過程中遇到的問題及解決辦法

測試覆蓋率在上線執行一段時間後,在實踐過程中發現了一些問題,總結為以下幾點:

4.1 在不同機器編譯會導致classid不一致的問題

在實踐過程中,經常遇到這樣一個問題,使用者反饋並確認案例已經正常執行,但是生成的報告顯示未覆蓋,經過調查發現在測試環境中的class和生成報告時的class不一致導致的。

在 JaCoCo內部,覆蓋率資料是以classid作為key來儲存的,classid是根據class的位元組碼hash演演算法得出來的,看JaCoCo原始碼中關於classid的演演算法如下:

圖片

出現不一致的情況包括:

  • 釋出時編譯的機器和生成報告的機器環境上有差異,比如作業系統版本、JDK版本等,導致編譯的class不一致;

  • 釋出時編譯的程式碼版本與生成報告時的程式碼版本有差異,導致編譯的class不一致。

 

要解決上面環境的問題,需要保持在測試覆蓋率過程中編譯的機器環境保持一致,或者做到只編譯一次,使用同一份class檔案,考慮到儲存空間的問題,vivo採用保持環境一致的辦法來解決。

對於第二種情況,常見於採用敏捷研發的團隊,在一個版本中按功能點轉測,經常導致測試在測試過程中,原始碼已經發生了修改,生成報告時程式碼版本和釋出時的程式碼版本已經不一致,這種情況比較複雜,我們在下面會介紹。

4.2 在研發過程中更加關注增量程式碼的覆蓋率

在我們日常的研發活動中,對於全量程式碼更多使用自動化指令碼來回歸,而新研發的功能主要表現為增量程式碼,對於增量程式碼的覆蓋率情況更加關注, JaCoCo本身不支援增量程式碼的覆蓋率。

對於這個問題網上也有不少解決方案,基本都是基於git的版本差異,在生成報告時過濾掉沒有差異的類,形成兩份覆蓋率報告,一份是全量程式碼覆蓋率報告,一份是增量程式碼覆蓋率報告,而我們更希望在一份覆蓋率報告中呈現增量程式碼和全量程式碼的覆蓋情況,結合程式碼在全量報告中的覆蓋路徑分析遺漏的場景,同時能在報告中標註增量程式碼和增量程式碼的覆蓋情況,期望的效果如下圖所示:

圖片

圖片

為了達到上述效果,需要幾個改造步驟:

  • 計算出當前程式碼分支的變動情況,需要精確到程式碼行

  • 改造JaCoCo計算邏輯,針對增量程式碼單獨統計覆蓋率指標值

  • 改造JaCoCo報告格式,在報告中相容全量程式碼和增量程式碼的覆蓋情況

對於計算程式碼分支的變動情況,放棄 GitLab 提供的程式碼比對功能來獲取不同版本之前的差異資訊,如果版本之間差異太多的話,經常發生GitLab 的API介面呼叫超時;

並且GitLab 的比對功能無法滿足客製化場景,比如一行程式碼僅僅因為格式化被識別為變更程式碼等等,採用藉助Linux自帶的diff命令,實現程式碼差異比對的能力:

圖片

對於改造 JaCoCo計算邏輯,增加針對增量程式碼的覆蓋率指標統計,在CoverageNodeImpl類中增加新的Counter,用於統計新增類、方法、行、指令覆蓋率指標;在SourceNodeImple類中increment方法中增加新增程式碼行的統計邏輯。

圖片

圖片

4.3 重談關於classid的問題

在上面已經談到關於classid的問題,如果是環境問題是比較好解決,但是現在網際網路團隊基本都使用敏捷模式,基本不太可能等開發工作全部完成再轉測,這樣必然會導致最新的覆蓋率報告,會出現以類為單元的覆蓋率資料丟失,需要測試人員來回重複的執行測試案例,否則測試覆蓋率資料不會很好看。

既然知道問題所在,那有沒有辦法解決呢?是不是可以直接找到以前的classid,把以前的classid對應的探針資料複製到當前的classid下就可以?當然是不行的,因為原始碼發生變動,導致探針的數量發生變化,會出現下面的情況:

圖片

或者這樣

圖片

出現這樣的情況,會無法判斷具體哪些探針是新增的或者刪除的;即使出現前後探針一致的情況,也有可能因為程式碼修改,探針位置發生變化:

圖片

那麼這個問題是否就無解了呢?這裡給出一個大概思路,現在的覆蓋率資料是以類為單位儲存的,我們可以修改儲存的粒度,細化到方法級別,這樣可以保留一個類的大部分探針資料,這樣如果只是修改一個方法的話,那麼其他方法的測試資料可以繼續保留,只需要重新測試這個方法就行,這樣可以有效的降低測試人員對整個類的所有方案重複測試的情況。

五、總結

對於測試覆蓋率功能,有沒有給測試的質量帶來提升,答案是顯而易見的。

當然也因為上面提到的問題,給測試人員帶了些麻煩,為了提升測試覆蓋率資料,導致測試人員對同一個功能重複多次測試;同時也給測試人員帶來了好處,很多測試人員在面對測試覆蓋率指標嚴格要求下,被迫去看程式碼的實現邏輯,提升了自己業務水平和閱讀程式碼的水平,甚至出現測試人員和開發人員當面對質,關於程式碼邏輯是否合理的場景。

最後,測試覆蓋率不是衡量測試質量的唯一標準,要合理利用測試覆蓋率來提升測試質量。