前言:
Android项目只能使用JaCoCo的离线插桩方式。为什么?主要是因为Android覆盖率的特殊性:
一般运行在服务器java程序的插桩可以在加载class文件进行,运用java
Agent的机制,可以理解成”实时插桩”。JaCoCo提供了自己的Agent,完成插桩的同时,还提供了丰富的dump输出机制,如File,Tcp
Server,Tcp Client。覆盖率信息可以通过文件或是Tcp的形式输出。这样外部程序可很方便随时拿到被测程序的覆盖率。但是Android系统破坏了JaCoCo这种便利性,原因有两个:
(1) Android虚拟机不同与服务器上的JVM,它所支持的字节码必须经过处理支持Android Dalvik等专用虚拟机,所以插桩必须在处理之前完成,即离线插桩模式。
(2) Android虚拟机没有配置JVM 配置项的机制,所以应用启动时没有机会直接配置dump输出方式。
一、项目结构
demo项目结构如下:
二、详细代码
InstrumentedActivity.java:
package com.example.jacocodemo.test;
import android.util.Log;
import com.example.jacocodemo.MainActivity;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class InstrumentedActivity extends MainActivity {
public static String TAG = "InstrumentedActivity";
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy()");
generateCoverageReport();
}
private void generateCoverageReport() {
String DEFAULT_COVERAGE_FILE_PATH = getFilesDir().getPath() + "/coverage.ec";
Log.d(TAG, "generateCoverageReport():" + DEFAULT_COVERAGE_FILE_PATH);
try {
OutputStream out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, 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));
out.close();
} catch (Exception e) {
Log.d(TAG, e.toString(), e);
}
}
}
JacocoInstrumentation.java:
package com.example.jacocodemo.test;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.io.IOException;
public class JacocoInstrumentation extends Instrumentation {
public static String TAG = "JacocoInstrumentation:";
private Intent mIntent;
@Override
public void onCreate(Bundle bundle) {
Log.d(TAG, "onCreate(" + bundle + ")");
super.onCreate(bundle);
String DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, "异常 : " + e);
e.printStackTrace();
}
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();//调用onStart
}
@Override
public void onStart() {
Log.d(TAG, "onStart()");
super.onStart();
startActivitySync(mIntent);
}
//adb shell am instrument com.tachibana.downloader/test.JacocoInstrumentation
}
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.jacocodemo">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.JacocoDemo">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".test.InstrumentedActivity"
android:label="InstrumentationActivity" />
</application>
<instrumentation
android:name=".test.JacocoInstrumentation"
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:targetPackage="com.example.jacocodemo" />
</manifest>
build.gradle改写:
apply plugin: 'com.android.application'
apply plugin: 'jacoco'//引用插件
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.jacocodemo"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'//instrument设置
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
testCoverageEnabled = true//JaCoCo功能启动
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
//ec文件解析函数
def coverageSourceDirs = [
'../app/src/main/java'
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories.from = fileTree(
dir: './build/intermediates/javac/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories.from = files(coverageSourceDirs)
executionData.from = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
doFirst {
new File("$buildDir/intermediates/javac/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation fileTree(dir: "libs", include: ["*.jar"])
}
三、操作步骤
- 安装debug包到设备上
- 退出app应用
- cmd命令:
adb shell am instrument com.example.jacocodemo/com.example.jacocodemo.test.JacocoInstrumentation
- 手动测试
- 返回键回到设备主页面
- 在设备的文件管理中取回代码覆盖率文件coverage.ec(路径:data/data/包名/files)
- 复制到项目的 build\outputs\code-coverage\connected 中
- 回到Android Studio,点击右侧的Gradle -> Tasks -> reporting -> jacocoTestReport
- 生成的报告(index.html)路径为:build/reports/jacoco/jacocoTestReport/html/com.example.jacocodemo 中
四、总结
测试覆盖率报告:(从右往左)
指标 | 内容 |
---|---|
类覆盖率 | \ |
方法覆盖率 | \ |
行覆盖率 | \ |
圈复杂度覆盖率 | 度量方法里所有可能的最小路径数是否被测试 |
逻辑分支覆盖率 | 度量方法里if和switch语句分支覆盖情况 |
指令覆盖率 | 度量单个java二进制代码指令的覆盖情况 |
拓展:接入Jenkins;增量/全量
参考资料:
https://www.jianshu.com/p/ab2be01d7347#comment-60999311
https://blog.csdn.net/itfootball/article/details/45618609