最近在做android测试时代码的覆盖率,网上查了好长一段时间,遇到了很多坑,在这里记录一下。
jacoco是Java Code Coverage的缩写,顾名思义,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理可以移步jacoco原理介绍。在安卓项目的代码覆盖率统计使用了jacoco的离线插桩方式,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试(单元测试、UI测试或者手工测试等)插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
这个网上有很多的例子,我这里也直接照搬过来。
在不修改android源码的情况下,在src/main/java 里面新增一个test目录 里面存放3个文件:FinishListener、InstrumentedActivity、JacocoInstrumentation,这三个文件的源码在网上有很多,我这里后面也会贴出来。
先看下目录结构:
FinishListener源码:
public interface FinishListener {
void onActivityFinished();
void dumpIntermediateCoverage(String filePath);
}
InstrumentedActivity源码:
import com.netease.coverage.jacocotest1.MainActivity;
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源码:
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
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) {
Log.d(TAG, "onCreate(" + arguments + ")");
super.onCreate(arguments);
DEFAULT_COVERAGE_FILE_PATH = getContext().getFilesDir().getPath().toString() + "/coverage.ec";
File file = new File(DEFAULT_COVERAGE_FILE_PATH);
if (file.isFile() && file.exists()){
if (file.delete()){
System.out.println("file del successs");
}else {
System.out.println("file del fail !");
}
}
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, "异常 : " + e);
e.printStackTrace();
}
}
if (arguments != null) {
mCoverageFilePath = arguments.getString("coverageFile");
}
mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
start();
}
@Override
public void onStart() {
System.out.println("onStart def");
if (LOGD)
Log.d(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) {
Log.d(TAG, e.toString(), e);
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;
Log.e(TAG, msg, e);
mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "
+ msg);
}
@Override
public void onActivityFinished() {
if (LOGD)
Log.d(TAG, "onActivityFinished()");
if (mCoverage) {
System.out.println("onActivityFinished mCoverage true");
generateCoverageReport();
}
finish(Activity.RESULT_OK, mResults);
}
@Override
public void dumpIntermediateCoverage(String filePath){
// TODO Auto-generated method stub
if(LOGD){
Log.d(TAG,"Intermidate Dump Called with file name :"+ filePath);
}
if(mCoverage){
if(!setCoverageFilePath(filePath)){
if(LOGD){
Log.d(TAG,"Unable to set the given file path:"+filePath+" as dump target.");
}
}
generateCoverageReport();
setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);
}
}
}
在app下面的build.gradle内需添加内容:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.4+"
}
buildTypes {
debug {
/**打开覆盖率统计开关**/
testCoverageEnabled = true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
与android平级 添加:
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 = fileTree(
dir: './build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
doFirst {
new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
AndroidManifest.xml添加配置:
在manifest标签中添加权限:
在application标签中添加activity
与application平级 添加JacocoInstrumentation声明(这里targetPackage 会报错忽略):
完整结构图如下:
其实写到这里基本已经完成了,可以安装包进行测试了(我用的是模拟器测试的)。
安装包需要注意:需要用installDebug 进行安装(刚开始路径可能不一样,可能在other里面)
apk包安装好以后,用adb 去启动程序进行测试,我这里执行的是:
adb shell am instrument com.netease.coverage.jacocotest1/com.netease.coverage.test.JacocoInstrumentation
(adb shell am instrument 项目所在的包/JacocoInstrumentation文件路径)
测试完后,按返回键 将app切入后台,即可在device monitor 的/data/data/项目文件/files 下找到coverage.ec文件,将这个文件导出。
3、生成报告html
先在gradle里面执行createDebugCoverageReport 在app/build/outputs里面生成code-coverage文件
将code-coverage/connected里面原始的ec文件删除,并加入刚才生成的coverage.ec文件。
执行gradle jacocoTestReport 生成报告,路径:app/build/reports/jacoco/jacocoTestReport/html/index.html
遇到的问题总结:
1、未找到org.jacoco.agent.rt.RT :
因为gradle已经集成了jacoco,但是作为刚接触jacoco的新手时,遇到这样的错误,只好自己去jacoco官网下了包 导入进来发现了另一个问题:JaCoCo agent not started. 还特意去查看源码:
返回的是jvm中的agent,但是显然是并没有agent在jvm中导致的错误。在网上找了好久都没能找到问题的解决方案,后来仔细想了想 可能是启动方式不对?想了想第一次是用robotium写了一个例子是成功得到了报告,在gradle中找到了用installDebug的安装方式,试了试果然可以生产ec文件。