閱讀本文大約需要2.1分鐘。
點選?小卡片,回複 “合集” 擷取系統性的學習筆記和測試開發技能圖譜
前言
JaCoCo的概念我就不在這裡複述了網上有很多資料介紹,這裡主要提一下他的兩種插樁模式:On-the-fly和Offline
On-the-fly模式:
JVM中通過-javaagent參數指定特定的jar檔案啟動Instrumentation的代理程式,代理程式在通過Class Loader裝載一個class前判斷是否需要轉換修改class檔案,然後将統計代碼插入class,測試覆寫率分析可以在JVM執行測試代碼的過程中完成。
Offline模式:
在測試前先對檔案進行插樁,然後生成插過樁的class或jar包,測試插過樁的class和jar包後,會生成動态覆寫資訊到檔案,最後統一對覆寫資訊進行處理,并生成報告。
但在Android項目中隻能使用JaCoCo的離線插樁模式,主要是因為Android系統破壞了JaCoCo的這種便利性,原因如下:
- Android虛拟機跟運作在伺服器上的JVM不同,它所支援的位元組碼必須經過特殊的處理以支援Dalvik、ART等虛拟機,是以插樁必須在處理之前完成;
- Android虛拟機無法像伺服器上的JVM那樣可以通過參數的方式實作配置,是以應用啟動的時候是沒有機會直接配置dump輸出方式擷取覆寫率資訊的;
背景
其實主要是基于兩個痛點:
1、新功能測試和回歸測試在手工測試的情況下,即便用例寫的再怎麼詳細,也經常會有漏測的發生,這裡一方面是因為現在大量網際網路公司采用外包資源來做業務測試,而外包的工作品質無法有效評估,可能存在漏執行的情況,另外一方面是本身測試用例設計的不夠完善導緻沒有覆寫到一些關鍵路徑的代碼分支,是以亟需一種可以度量手工測試完成後對代碼覆寫情況的手段或者工具;
2、研發代碼變更的影響範圍難以精準評估,比如研發送出一個MR,這個MR到底影響了多少用例,在沒有精準測試能力的情況下是很難給出的,而做精準測試,最重要的一環就是代碼用例的關系庫維護,如何生成代碼跟用例的關系,就需要用到代碼覆寫率的采集和分析能力了;
實戰
其實基于jacoco來做Android端代碼覆寫率的難點主要是各個項目的gradle插件依賴跟jacoco版本直接的相容性問題,特别是在以及開發很多年的多子產品項目下,這個問題尤為明顯,另外網上雖然有很多相關的文章資料,但是要麼是gradle插件依賴版本太低,要麼就是jacoco版本、配置檔案以及項目的開發環境沒有說清楚或者寫的有問題,導緻最終很難按照說明完成接入。
是以我先說明一下我的依賴情況,我用的是4.0版本比較新,應該算是目前主流的項目開發環境了:
gradle插件版本:classpath 'com.android.tools.build:gradle:4.0.1'
gradle依賴版本:distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
複制
我這裡直接以多子產品項目為例,單子產品項目修改jacoco.gradle配置檔案中的源碼路徑和class檔案路徑即可。
第一步
在app子產品下建立一個jacoco.gradle檔案,具體代碼如下所示:
apply plugin: 'jacoco'
android {
buildTypes {
debug {
/**打開覆寫率統計開關**/
testCoverageEnabled = true
}
}
}
//源代碼路徑,有多少個module,就在這裡寫多少個路徑,如果你隻有app一個module,那麼就寫一個就可以
def coverageSourceDirs = [
'../app/src/main/java',
'../common/src/main/java',
]
//class檔案路徑,如果你隻有app一個module,那麼就寫一個就可以
def coverageClassDirs = [
'/app/build/intermediates/javac/debug/classes',
'/common/build/intermediates/javac/debug/classes',
]
//Jacoco 版本,建議用這個版本相容性比較好
jacoco {
toolVersion = "0.8.2"
}
//生成報告task
task jacocoTestReport(type: JacocoReport) {
group = "JacocoReport"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.from = files(files(coverageClassDirs).files.collect {
println("$rootDir" + it)
fileTree(dir: "$rootDir" + it,
// 過濾不需要統計的class檔案
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
})
sourceDirectories.from = files(coverageSourceDirs)
executionData.from = files("$buildDir/outputs/code-coverage/coverage.ec")
doFirst {
coverageClassDirs.each { path ->
println("$rootDir" + path)
new File("$rootDir" + path).eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
}
//初始化Jacoco Task
task jacocoInit() {
group = "JacocoReport"
doFirst {
File file = new File("$buildDir/outputs/code-coverage/")
if (!file.exists()) {
file.mkdir();
}
}
}
複制
其中class的檔案路徑,具體跟gradle的版本有關,需要檢視你自己實際的路徑,如下圖:
然後在你的app子產品下的build.gradle檔案中依賴這個jacoco.gradle,如下所示:
apply from: 'jacoco.gradle'
...do something
android {
...
}
複制
我們再整理一個jacoco.gradle放在項目的根目錄作為通用配置,内容如下:
apply plugin: 'jacoco'
android {
buildTypes {
debug {
/**打開覆寫率統計開關**/
testCoverageEnabled = true
}
}
}
複制
如果需要統計子module中的代碼覆寫率,那麼需要在子module的build.gradle檔案中添加如下依賴:
apply from: rootProject.file('jacoco.gradle')
複制
第二步
定義一個JacocoHelper類,主要是用來生成ec檔案,根據使用場景可以放在你需要的地方,比如在APP内提供一個按鈕,點選觸發生成ec檔案,也可以通過指令行的方式觸發,具體代碼如下:
package com.android.jarvis.jacoco;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class JacocoHelper {
private static final String TAG = "JacocoHelper";
//ec檔案的路徑
private static final String DEFAULT_COVERAGE_FILE_PATH = Environment.getExternalStorageDirectory()
.getPath() + "/coverage.ec";
/**
* 生成ec檔案
*
* @param isNew 是否重新建立ec檔案
*/
public static void generateEcFile(boolean isNew) {
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
try {
if (isNew && mCoverageFilePath.exists()) {
Log.d(TAG, "清除舊的ec檔案");
mCoverageFilePath.delete();
}
if (!mCoverageFilePath.exists()) {
mCoverageFilePath.createNewFile();
}
out = new FileOutputStream(mCoverageFilePath.getPath(), true);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
if (agent != null) {
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
}
} catch (Exception e) {
Log.d(TAG, e.toString());
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
複制
在需要生成ec檔案的地方調用下面的方法:
JacocoHelper.generateEcFile(true);
複制
生成測試報告
通過上面的兩個步驟,我們就完成了Android項目的Jacoco配置,下面再教大家如何使用它來擷取我們手工或者自動化測試的代碼覆寫率。
首先我們可以通過Android Studio直接編譯安裝內建了Jacoco的Debug包,然後再在項目的根目錄執行下面的指令完成初始化:
./gradlew jacocoInit
複制
接着我們就可以通過執行自動化測試腳本或者手工來開始我們的用例測試了,測試完成後執行下面的指令:
adb pull /storage/emulated/0/coverage.ec .
複制
把得到的coverage.ec檔案放到下圖所示的位置,其中code-coverage目錄就是執行初始化腳本生成的。
最後我們在項目根目錄執行下面的指令來生成報告:
./gradlew jacocoTestReport
複制
在下圖所示位置,我們就可以看到覆寫率的報告了
注意:
上面的指令中用的是./gradlew,但也可以替換為gradle,兩者的差別這裡跟大家稍微解釋一下,gradlew其實就是對gradle的包裝和配置,gradlew是gradle Wrapper,Wrapper的意思就是包裝。
因為不是每個人的電腦中都安裝了gradle,也不一定安裝的版本就是要編譯項目需要的版本,那麼gradlew裡面就配置了項目需要的gradle版本,使用者隻需要運作gradlew就可以按照配置下載下傳對應的gradle到項目的目錄中,僅僅給項目本身用。
報告分析
生成的報告如下:
點選包名你可以看到類的覆寫率情況
再點選類名,可以看到具體哪些代碼被調用到了,又有哪些代碼沒有被調用到
落地場景
我們既然可以根據執行的用例拿到代碼的覆寫情況,那麼我們就可以基于這個資料來做度量和精準測試了,比如:
1、可以讓業務QA或者外包使用覆寫率包來完成功能子產品的測試工作,這樣就可以根據生成的覆寫率資料來度量測試效果了;
2、另外在做精準測試的時候,我們都需要維護用例和代碼的關系庫,那麼如何得到這個關系呢,這時候我們就可以通過在手工或者UI自動化的方式執行用例的過程中把每個用例跟對應覆寫的代碼類檔案建立映射關系來完成初期的關系庫;