Android+jacoco 实现覆盖率及问题记录
最近在做android测试时代码的覆盖率,网上查了好长一段时间,遇到了很多坑,在这里记录一下。
1、jacoco介绍:
2、手动测试生成coverage.ec文件
在不修改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标签中添加权限:
<uses-permission android:name="android.permission.USE_CREDENTIALS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.READ_PROFILE" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
在application标签中添加activity
<activity android:label="InstrumentationActivity" android:name="com.netease.coverage.test.InstrumentedActivity" />
与application平级 添加JacocoInstrumentation声明(这里targetPackage 会报错忽略):
<instrumentation android:handleProfiling="true" android:label="CoverageInstrumentation" android:name="com.netease.coverage.test.JacocoInstrumentation" android:targetPackage="com.netease.coverage.jacocotest1"/> <!-- 项目名称 -->完整结构图如下:
其实写到这里基本已经完成了,可以安装包进行测试了(我用的是模拟器测试的)。
安装包需要注意:需要用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文件。