Jacoco在Android系统应用测试中覆盖率一直为0的解决方案
问题
普通应用Gradle配置Jacoco,运行createDebugAndroidTestCoverageReport,能够正常输出覆盖率报告,报告路径为:
build/reports/coverage/debug/index.html。查看build/outputs/code-coverage/connected/*-coverage.ec,存在运行覆盖数据。
android { compileSdkVersion 27 defaultConfig { applicationId "example.api.com.myapplication2" minSdkVersion 24 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { debug{ testCoverageEnabled true } } }
在AndroidManifest中增加android:sharedUserId=”android.uid.system”
,输出报告覆盖率一直为0。查看build/outputs/code-coverage/connected/*-coverage.ec 一直为空。
Jacoco原理
参考:https://blog.csdn.net/allan_shore_ma/article/details/80053340
简单来说:
debug模式下增加testCoverageEnabled true 后Gradle自动利用 jacoco插件在生成最终的目标文件之前,对源app Class文件进行插桩,生成最终的目标文件,执行目标文件以后得到覆盖执行数据。通过执行createDebugAndroidTestCoverageReport,在测试程序结束后会通过反射调用org.jacoco.agent.rt.RT.getAgent().getExecutionData(false) 获取到二进制数据并将此文件数据同步到uild/outputs/code-coverage/connected/${deviceName}-coverage.ec中。然后Gralde将此文件与build/intermediates/classes文件进行比较标记出类和行号,同时与src对比,输出报告到build/reports/coverage/debug/index.html
解决办法
因为代码插桩原理都是一样的,运行后也会有jacoco的覆盖数据信息。但实际系统应用获取到的coverage.ec为空,可以猜测以为权限问题,此数据无法直接导入到build/outputs/code-coverage/connected/*-coverage.ec。系统应用以及它的测试应用已经有写sd卡权限,可以通过sd卡作为中转。在测试用例执行完成的恰当时机,刷新sd卡的coverage.ec。测试结束后导出数据到build/outputs/code-coverage/connected/coverage.ec。最后执行jacoco的生成报告任务。
在测试用例执行完的恰当时机刷新coverage.ec,主要代码:
public class PushTestReport { public static String getSDPath(){ File sdDir = null; boolean sdCardExist = Environment.getExternalStorageState() .equals(android.os.Environment.MEDIA_MOUNTED); if(sdCardExist) { sdDir = Environment.getExternalStorageDirectory(); } return sdDir.toString(); } public static void createReport() { String dir = getSDPath(); String path = dir + "/coverage.ec"; Log.d("jacoco","createReport:"+path); File file = new File(path); if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); Log.e("jacoco","pushReport,error:"+e.getMessage()); } } OutputStream out = null; try { out = new FileOutputStream(path, false); Object agent = Class.forName("org.jacoco.agent.rt.RT") .getMethod("getAgent") .invoke(null); byte[] bytes = (byte[])agent.getClass().getMethod("getExecutionData", boolean.class) .invoke(agent, false); Log.d("jacoco","pushReport,bytes:"+bytes.length); out.write(bytes); } catch (Exception e) { e.printStackTrace(); Log.e("jacoco","pushReport,error:"+e.getMessage()); } finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } }}
调用时机tearDown():
@After public void tearDown() throws Exception { PushTestReport.createReport(); }
以及TestRule运行完用例处:
@Override public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { base.evaluate(); PushTestReport.createReport(); } }; }
build.gradle如下:
task preJacocoTestReport(type: Exec) { executable 'sh' args "pre-jacoco-report.sh"}task jacocoTestReport(type: JacocoReport,dependsOn: preJacocoTestReport) { group = "Reporting" description = "Generate Jacoco coverage reports after running tests." reports { xml.enabled = true html.enabled = true } def coverageSourceDirs = [ '../app/src/main/java' ] def excludeClasses = ['**/R*.class', '**/*$InjectAdapter.class', '**/*$ModuleAdapter.class', '**/*$ViewInjector*.class', '**/BuildConfig.class' ] classDirectories = fileTree(dir: '../app/build/intermediates/classes/debug',excludes: excludeClasses) 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('$$', '$')) } } }}
pre-jacoco-report.sh脚本:
#!/usr/bin/env bash#当前在环境为Project/app目录rm -f build/outputs/code-coverage/connected/coverage.ecadb pull /storage/emulated/0/coverage.ec build/outputs/code-coverage/connected/
运行以下任务输出报告:build/reports/jacoco/jacocoTestReport/html/index.html
gradle createDebugCoverageReport -p Project/appgradle jacocoTestReport -p Project/app
子模块覆盖率统计注意
如果想要子模块的覆盖率也统计进去
-
子模块的build.gradle中也增加
testCoverageEnabled true
-
src也要包含子模块的src,例如:
def coverageSourceDirs = [
‘…/**/src/main/java’
] -
class也要包含子模块的class:
classDirectories += fileTree(dir: ‘…/subMoudle1/build/intermediates/classes/debug’,excludes: excludeClasses)
classDirectories += fileTree(dir: ‘…/subMoudle2/build/intermediates/classes/debug’,excludes: excludeClasses)
插曲
Jacoco一直报错:
08-20 13:47:35.971 25869-25869/com.gs.service W/System.err: java.io.FileNotFoundException: /jacoco.exec (Read-only file system) at java.io.FileOutputStream.open(Native Method) at java.io.FileOutputStream.(FileOutputStream.java:221) at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.openFile(FileOutput.java:67) at org.jacoco.agent.rt.internal_8ff85ea.output.FileOutput.startup(FileOutput.java:49)08-20 13:47:35.972 25869-25869/com.gs.service W/System.err: at org.jacoco.agent.rt.internal_8ff85ea.Agent.startup(Agent.java:122) at org.jacoco.agent.rt.internal_8ff85ea.Agent.getInstance(Agent.java:50) at org.jacoco.agent.rt.internal_8ff85ea.Offline.(Offline.java:31) at org.jacoco.agent.rt.internal_8ff85ea.Offline.getProbes(Offline.java:51)
修改文件权限或者增加/jacoco-agent.properties
里面destfile=/mnt/sdcard/jacoco.exec
并不能解决问题。然而此异常也并不影响覆盖率的统计。后续仍可深究。
更多相关文章
- Android之R文件
- Android第五个功能:文件存储到SDCard上面
- Android Sqlite Failed to open database(无法打开数据库文件)
- android tips:从资源文件中读取文件流显示
- Android的CheckBox控件的点击效果布局文件
- Android程序调试时生成main.out.xml文件
- android 拨号盘Contact模块讲解(四)
- 利用第三方jar包jaudiotagger实现与MediaMetadataRetriever类似
- 在Android中实现文件读写