一、前言
JaCoCo 是一個免費的 Java 代碼覆蓋率庫,由 EclEmma 團(tuán)隊根據(jù)多年使用和集成現(xiàn)有庫的經(jīng)驗教訓(xùn)創(chuàng)建。官網(wǎng)地址
JaCoCo是面向Java的開源代碼覆蓋率工具,JaCoCo以Java代理模式運行,它負(fù)責(zé)在運行測試時檢測字節(jié)碼。 JaCoCo會深入研究每個指令,并顯示每個測試過程中要執(zhí)行的行。 為了收集覆蓋率數(shù)據(jù),JaCoCo使用ASM即時進(jìn)行代碼檢測,并在此過程中從JVM Tool Interface接收事件,最終生成代碼覆蓋率報告。
Jacoco運行有離線(offline)、在線(on the fly)模式之說,所謂在線模式就是在應(yīng)用啟動時加入jacoco agent進(jìn)行插樁,在開發(fā)、測試人員使用應(yīng)用期間實時地進(jìn)行代碼覆蓋率分析。相信很多的java項目開發(fā)人員并不會去寫單元測試代碼的,因此覆蓋率統(tǒng)計就要把手工測試或接口測試覆蓋的情況作為重要依據(jù),顯然在線模式更符合實際需求。
二、AndroidStudio 集成Jacoco
- 環(huán)境
- 根目錄build.gradle
buildscript {
dependencies {
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3'
classpath "org.jacoco:org.jacoco.core:0.8.8"
}
}
plugins {
id 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.20' apply false
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
- gradle依賴版本
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
- 編譯sdk
compileSdk : 33
minSdk : 21
targetSdk : 33
3、 配置jacoco
- 在項目根目錄新建jacoco.gradle 文件
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.8"
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
ext {
getFileFilter = { ->
def jacocoSkipClasses = null
if (project.hasProperty('jacocoSkipClasses')) {
jacocoSkipClasses = project.property('jacocoSkipClasses')
}
//忽略類文件配置
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*$ViewInjector*.*']
if (jacocoSkipClasses != null) {
fileFilter.addAll(jacocoSkipClasses)
}
return fileFilter
}
}
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports"
reports {
xml.enabled(true)
html.enabled(true)
}
def fileFilter = project.getFileFilter()
println("files:"+fileFilter)
def coverageClassDirs = ["$project.buildDir/intermediates/javac/debug/classes"
, "$rootDir/module_data/build/intermediates/javac/debug/classes"]
getClassDirectories().setFrom(files(files(coverageClassDirs).files.collect {
println("class:"+it)
fileTree(dir: it, excludes: fileFilter)
}))
def coverageSourceDirs = ["$project.projectDir/src/main/java", "$rootDir/module_data/src/main/java"]
//設(shè)置需要檢測覆蓋率的目錄
println("source:"+coverageSourceDirs)
getSourceDirectories().setFrom(files(coverageSourceDirs))
def executionDirs=["$project.projectDir/build/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"]
println("exe:"+executionDirs)
//以下路徑也需要檢查
// getExecutionData().setFrom(fileTree(dir: project.buildDir, includes: ['outputs/code-coverage/debugAndroidTest/connected/coverage.ec']))
getExecutionData().setFrom(files(executionDirs))
doFirst {
//遍歷class路徑下的所有文件,替換字符
coverageClassDirs.each { path ->
new File(path).eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
}
- 生成ec 文件。
在app/module 包下新建test目錄,新建如下類
FinishListener
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
InstrumentedActivity
public class InstrumentedActivity extends MainActivity {
public FinishListener finishListener;
public void setFinishListener(FinishListener finishListener) {
this.finishListener = finishListener;
}
@Override
public void onDestroy() {
if (this.finishListener != null) {
finishListener.onActivityFinished();
}
super.onDestroy();
}
}
JacocoInstrumentation
public class JacocoInstrumentation extends Instrumentation implements FinishListener {
public static String TAG = "JacocoInstrumentation:";
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
private final Bundle mResults = new Bundle();
private Intent mIntent;
private static final boolean LOGD = true;
private boolean mCoverage = true;
private String mCoverageFilePath;
public JacocoInstrumentation() {
}
@Override
public void onCreate(Bundle arguments) {
LogUtil.e(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (file.isFile() && file.exists()) {
if (file.delete()) {
LogUtil.e(TAG, "file del successs");
} else {
LogUtil.e(TAG, "file del fail !");
}
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
LogUtil.e(TAG, "異常 : " + e);
e.printStackTrace();
}
}
if (arguments != null) {
LogUtil.e(TAG, "arguments不為空 : " + arguments);
mCoverageFilePath = arguments.getString("coverageFile");
LogUtil.e(TAG, "mCoverageFilePath = " + mCoverageFilePath);
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
@Override
public void onStart() {
LogUtil.e(TAG, "onStart def");
if (LOGD) {
LogUtil.e(TAG, "onStart()");
}
super.onStart();
Looper.prepare();
InstrumentedActivity activity = (InstrumentedActivity) startActivitySync(mIntent);
activity.setFinishListener(this);
}
private boolean getBooleanArgument(Bundle arguments, String tag) {
String tagString = arguments.getString(tag);
return tagString != null && Boolean.parseBoolean(tagString);
}
private void generateCoverageReport() {
OutputStream out = null;
try {
out = new FileOutputStream(getCoverageFilePath(), false);
Object agent = Class.forName("org.jacoco.agent.rt.RT")
.getMethod("getAgent")
.invoke(null);
out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
.invoke(agent, false));
} catch (Exception e) {
LogUtil.e(TAG, e.toString());
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String getCoverageFilePath() {
if (mCoverageFilePath == null) {
return DEFAULT_COVERAGE_FILE_PATH;
} else {
return mCoverageFilePath;
}
}
private boolean setCoverageFilePath(String filePath) {
if (filePath != null && filePath.length() > 0) {
mCoverageFilePath = filePath;
return true;
}
return false;
}
private void reportEmmaError(Exception e) {
reportEmmaError("", e);
}
private void reportEmmaError(String hint, Exception e) {
String msg = "Failed to generate emma coverage. " + hint;
LogUtil.e(TAG, msg);
mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
+ msg);
}
@Override
public void onActivityFinished() {
if (LOGD) {
LogUtil.e(TAG, "onActivityFinished()");
}
if (mCoverage) {
LogUtil.e(TAG, "onActivityFinished mCoverage true");
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath) {
// TODO Auto-generated method stub
if (LOGD) {
LogUtil.e(TAG, "Intermidate Dump Called with file name :" + filePath);
}
if (mCoverage) {
if (!setCoverageFilePath(filePath)) {
if (LOGD) {
LogUtil.e(TAG, "Unable to set the given file path:" + filePath + " as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
配置AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<activity
android:name=".test.InstrumentedActivity"
android:label="InstrumentationActivity" />
</application>
<instrumentation
android:name=".test.JacocoInstrumentation"
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:targetPackage="com.qianyin.user" />
三、生成測試報告
installDebug
我們通過命令行安裝app,選擇你的app -> Tasks -> install -> installDebug,安裝app到你的設(shè)備上。命令行啟動app
先列出手機中已安裝的instrumentation:
adb shell pm list instrumentation
啟動app
adb shell am instrument com.qianyin.user/.test.JacocoInstrumentation
- 正常點擊測試app的功能
這個時候你可以操作你的app,對你想進(jìn)行代碼覆蓋率檢測的地方,進(jìn)入到對應(yīng)的頁面,點擊對應(yīng)的按鈕,觸發(fā)對應(yīng)的邏輯,你現(xiàn)在所操作的都會被記錄下來,在生成的coverage.ec文件中都能體現(xiàn)出來。當(dāng)你點擊完了,根據(jù)我們之前設(shè)置的邏輯,當(dāng)我們MainActivity執(zhí)行onDestroy方法時才會通知JacocoInstrumentation生成coverage.ec文件,我們可以按返回鍵退出MainActivity返回桌面,生成coverage.ec文件可能需要一點時間哦(取決于你點擊測試頁面多少,測試越多,生成文件越大,所需時間可能多一點)
然后在Android Studio的Device File Explore中,找到data/data/包名/files/coverage.ec文件,右鍵保存到桌面?zhèn)溆?/p>
-
createDebugCoverageReport
image.png
雙擊它,會執(zhí)行創(chuàng)建覆蓋率報告的命令,等待它執(zhí)行完,這個會生成一個coverage.ec文件,但是這個不是我們最終需要分析的,我們需要分析的是我們剛才手動點擊保存到桌面的那個。將保存到桌面的ec文件覆蓋這個ec文件。
image.png
備注
jacoco.gradle 文件中g(shù)etExecutionData().setFrom(files(executionDirs)) 中 文件目錄要和coverage.ec文件的目錄保持一致否則會有問題。
-
jacocoTestReport
image.png
找到這個路徑,雙擊執(zhí)行這個任務(wù),會生成我們最終所需要代碼覆蓋率報告,執(zhí)行完后,我們可以在這個目錄下找到它。生成文件的目錄可以自定義
app/build/reports/jacoco/jacocoTestReport/html/index.html
-
分析報告
image.png
四、問題
1、執(zhí)行 jacocoTestReport task 出現(xiàn) Task :app:jacocoTestReport SKIPPED
可能原因:在 gradle 配置的路徑,比如上面的 app/build/outputs/code-coverage/connected/ 路徑創(chuàng)建失敗或有錯誤;檢查路徑是否有拼寫錯誤,是否與配置的一致。該路徑在配置時可自定義,在定義后存放 .ec 文件的路徑需與此保持一致!
2、gradle 的更新,一些gradle方法已經(jīng)過時,建議更新成最新方法使用,如getClassDirectories();getSourceDirectories();getExecutionData()



