? ? ? ? JaCoCo(Java Code Coverage)是一款開源的代碼覆蓋率分析工具,適用于Java和Android項目。它通過插樁技術統計測試過程中代碼的執行情況,生成可視化報告,幫助開發者評估測試用例的有效性。在github上開源的項目: GitHub - jacoco/jacoco: :microscope: Java Code Coverage Library ,是針對服務端的,而移動端的jacoco插件暫時沒有開源,可以參考: The JaCoCo Plugin ,使用最多的版本是0.8.7,你也可以嘗試使用最新版本。
2.2.1 Jacoco插件的接入
將要接入Jacoco插件的一個Android應用,或是從github上下載一個Demo來進行測試,不過網上的Demo可能因為gradle或是其他包的版本不兼容最新的版本,需要先進行處理一下,能打包后再進行接入jacoco插件。現在我以一個簡單的Android計算器的Demo做一個jacoco接入的演示,早期github上的項目地址是 https://github.com/FlamingJay/AndroidCalculator.git,后來被刪除了,后面我將上傳到我的github上供大家學習。

- build.gradle中添加jacoco插件
在app下的build.gradle文件中添加對jacoco的引用,如下所示:
plugins {id 'com.android.application'id 'jacoco'
}jacoco {toolVersion = "0.8.7" // 選擇合適的版本
}
注意:此處使用的是0.8.7版本,這個版本比較穩定,你也可以使用最新版本。
- 打開覆蓋率采集開關
-
android {...buildTypes {release {minifyEnabled falsetestCoverageEnabled = trueproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}debug {testCoverageEnabled = true}}...}
- 一般在debug包下進行覆蓋率的測試,打開testCoverageEnabled = true, 構建項目的時候就能對代碼進行插樁,采集覆蓋率數據。
- release包有代碼混淆,覆蓋率報告渲染的時候,無法正確對應到類的源碼,所以要對release包進行測試時,需要關掉代碼混淆。
通過上面的配置,打包后的App就可以采集覆蓋率數據,記錄用戶的具體操作覆蓋。注意:此時的覆蓋率數據存在于內存中,要想拿到覆蓋率數據,必須人為地將覆蓋率數據寫入到文件中。
2.2.2 覆蓋率數據采集
由于覆蓋率數據內容存在于手機內存中,當App退出后,內存中的數據將被清空。而我們要進行覆蓋率測試的時候,必須要拿到覆蓋率數據文件,下面我們將借助于jacoco將覆蓋率數據從內容寫入到文件中,代碼如下:
package com.example.calculator.utils;import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;public class GenerateECFile {public static String TAG = "GenerateECFile:";private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";private static String partStr="coverage";public static List<File> getDeletePath(Context context) {List<File> files = new ArrayList<>();File sdDir = new File(context.getFilesDir().getPath());String[] list = sdDir.list();if (list != null) {for(int i=0;i<list.length;i++){if(list[i].contains(partStr)){files.add(new File(sdDir.getPath() + "/" + list[i]));}}}return files;}/*** 刪除覆率數據文件* @param context*/public static void deleteCoverageFiles(Context context){List<File> files = getDeletePath(context);if (files!= null && files.size() > 0) {for(File file:files){Log.d(TAG, "JacocoUtils_generateEcFile: 清除舊的ec文件path:+"+ file.getPath());// FileUtils.deleteFile(file);boolean result = file.delete();if (!result && file.exists()) {try{throw new IOException("Failed to delete " + file.getAbsolutePath());}catch(IOException e){e.printStackTrace();}}}}}public static void onJacocoCreate(Context context) {Log.d(TAG, "onJacocoCreate");SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");Calendar cal = Calendar.getInstance();String create_time = format.format(cal.getTime()).substring(0,19);// 獲取packagemanager的實例PackageManager packageManager = context.getPackageManager();// getPackageName()是你當前類的包名,0代表是獲取版本信息try{//刪除原來的覆蓋率數據文件deleteCoverageFiles(context);//生成新覆蓋率數據文件名PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(),0);String app_version = packInfo.versionName;DEFAULT_COVERAGE_FILE_PATH = context.getFilesDir().getPath() + "/coverage"+"-"+app_version+"-"+create_time+".ec";}catch(PackageManager.NameNotFoundException e){e.printStackTrace();Log.d(TAG,"找不到包名"+e);}}/*** 生成覆蓋率數據文件* @param context*/public static void generateCoverageFile(Context context) {OutputStream out = null;try {//如果文件不存在,創建覆蓋率數據文件File file = new File(DEFAULT_COVERAGE_FILE_PATH);if(!file.exists()){try{file.createNewFile();}catch (IOException e){Log.d(TAG,"新建文件異常:"+e);e.printStackTrace();}}//將內存中的覆蓋率數據寫入到文件中out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, true);Object agent = Class.forName("org.jacoco.agent.rt.RT").getMethod("getAgent", new Class[0]).invoke(null);out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class).invoke(agent, false));Log.d(TAG, "生成覆蓋率數據文件:"+DEFAULT_COVERAGE_FILE_PATH);} catch (Exception e) {Log.d(TAG, e.toString(), e);} finally {if (out != null) {try {out.close();} catch (IOException e) {e.printStackTrace();}}}}
}
說明:
- 本代碼借助于jacoco.agent將手機內存中的覆蓋率數據文件寫入到文件中;
- 默認文件路徑是本應用的files文件夾,由于現在高版本的android系統不允許訪問手機存儲,只能存儲到App的本身空間中,所以在AndroidManifest.xml文件中要添加:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
- 代碼中有初始化,清除原來覆蓋率文件的函數,也有生成覆蓋率文件的函數,只要在適合的時機調用即可。
- 文件命名:coverage-app版本號-日期-時間.ec 如:coverage-1.0-2025-02-11-10_50_03.ec。
2.2.3 何時生成覆蓋率文件?
通過專門的類,可以將手機中的覆蓋率數據寫入到應用的空間中,保存成覆蓋率文件。現在存在一個問題,什么時候保存覆蓋率數據文件?由于覆蓋率數據存在于內存中,一旦應用退出 ,數據將被清除。分析一下app的生命周期,不難發現:
- 在app進入前端時,清除原來的覆蓋率數據文件,開始采集覆蓋率數據;
- 在app進入后臺時,生成覆蓋率數據文件。
這樣交互進行比較合適。如果你的應用中有生命周期控制類,在相應的函數中引用上面的覆蓋率生成函數即可,如果沒有,請按如下方法,在MainActivity中的onCreate函數中添加生命周期控制函數,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);.....// 注冊LifecycleObserverProcessLifecycleOwner.get().getLifecycle().addObserver(new LifecycleObserver() {@OnLifecycleEvent(Lifecycle.Event.ON_START)public void onMoveToForeground() {//清除原來的覆蓋率數據文件GenerateECFile.onJacocoCreate(MainActivity.this);// 應用從后臺移動到前臺時調用Log.i(TAG,"App moved to foreground");}@OnLifecycleEvent(Lifecycle.Event.ON_STOP)public void onMoveToBackground() {//開始生成覆蓋率數據文件GenerateECFile.generateCoverageFile(MainActivity.this);// 應用從前臺移動到后臺時調用Log.i(TAG,"App moved to background");}});....}
?添加了以上操作,就可以在App的生命周期中采集覆蓋率數據,并寫入到文件中。
將通過上面修改的app打包,安裝到手機上進行測試。打開計算器,隨便進行一些操作或是執行一些測試用例,然后再將app置于后臺,注意不要殺死App。此時會將前面操作的覆蓋率數據寫入數據文件,置到后臺一會兒后,再殺死應用,就可以拿覆蓋率數據文件了。
2.2.4 下載覆蓋率數據文件
根據設置覆蓋率數據文件會生成在手機下面的位置:/data/data/應用包名/files,但是正常的手機系統由于安全設置,是無法下載下來的:

此時,在Android Studio點擊右側的Device Manager,找到連接的手機設備,單擊設備最右側的按鈕,選擇"Open in Device Explorer",就可以打開手機的文件系統,如下所示:

找到對應的覆蓋率數據文件的位置,如:/data/data/com.example.calculator/files/,就可以看到覆蓋率數據文件,右擊文件,選擇"save as..".將覆蓋率數據文件下載到本地目錄。

下載到覆蓋率數據文件后,就可以根據需要生成全量和增量覆蓋率報告,檢測測試情況,排查漏測問題補充測試用例。