Android+jacoco实现代码覆盖率最正确的实现方式,没有之一!

Android+jacoco实现代码覆盖率最正确的实现方式,没有之一!前言:jacoco是JavaCodeCoverage的缩写,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理介绍的文章在网上有很多,感兴趣的同学可以去找别的博客看看,我这里不做赘述。它的作用是在安卓项目的代码覆盖率统计使用了jacoco的离线插桩方式,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试(单元测试、UI测试或者手工测试等)插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。在我接到这个需求,需要统计开发人.

大家好,又见面了,我是你们的朋友全栈君。

前言 :jacoco是Java Code Coverage的缩写,是Java代码覆盖率统计的主流工具之一。关于jacoco的原理介绍的文章在网上有很多,感兴趣的同学可以去找别的博客看看,我这里不做赘述。它的作用是在安卓项目的代码覆盖率统计使用了jacoco的离线插桩方式,在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试(单元测试、UI测试或者手工测试等)插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。

在我接到这个需求,需要统计开发人员提交代码自测率的时候,从其他渠道和gradle推荐了解到的实现方式都是jacoco,然后也上网查了不少的资料,网上的资料都非常老了,gradle插件依赖的不是1.+就是2.+,gradle依赖还是4.4左右,所以导致一个问题,也是浪费了我很多时间的问题:网上的资料已经跟不上时代了,然而没有一篇最新的、最正确的jacoco+Android集成实践的博文,来给有这方面有诉求的同学指引方向,在我费尽千辛万苦终于找到突破口并实现了之后,决定记录这个问题,为日后有需求的同学点一盏明灯!

首先标明我的使用环境,应该也是现在主流的项目开发环境,也比较新:

1.gradle插件版本:
	classpath 'com.android.tools.build:gradle:3.5.1'(根目录build.gradle)
	
2.gradle依赖版本:
	distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip(gradle-wrapper.properties)
	
3.android sdk版本:
	minSdkVersion 19
	targetSdkVersion 28(app模块build.gradle)

下面我来说说网上现存博文的问题,文末会附上完整版实现代码。

一、踩坑记录

1、classDirectories路径不正确

以网上代码为例,都是这么写的:

classDirectories = fileTree(dir: "./build/intermediates/classes/debug", excludes: excludesFilter)

首先这个路径是gradle版本比较老的情况下,执行编译的时候才会在这个路径下生成class文件,但是在我使用的开发环境基础上,app/build/intermediates/classes根本就没有内容,这个问题阻拦了我很长一段时间,一度让我郁闷,以为是哪里配置出了问题,导致我的工程无法正确的生成class文件!

但是当我了解到是gradle版本的区别后,新版本的gradle在编译源代码时,生成的路径根本就不是这个,正确的路径是:

app/build/intermediates/javac/debug/classes

一切豁然开朗!!!在这个目录下,我找到了我需要了一切classes文件。

2、多module依赖覆盖率统计

这是第二个让我郁闷的地方,翻遍网上介绍的博文,凡是涉及多个库依赖统计覆盖率的,千篇一律的实现方式是这样:
将moduleA中原来依赖方式由:

compile project(':moduleB')

变成:

debugCompile project(path:':moduleB',configuration:'debug')

为此我还查了怎么让lirbray工程编译提供debug包的方式,然而在我使用configuration的时候,编译就是不通过,大哥,compile依赖都是多久年之前的依赖方式了,现在都用implementation或者api替代了好吗,另外configuration已经不存在了,所以这种依赖module的方式根本不可用!!!

我们在依赖的需要统计覆盖率的module对应的build.gradle中,只需要添加几个地方,可以整理一个jacoco-config.gradle文件,代码如下:

apply plugin: 'jacoco'
android { 
   
  defaultPublishConfig "debug"
  buildTypes { 
   
    debug { 
   
      /**打开覆盖率统计开关**/
      testCoverageEnabled = true
    }
  }
}

在你需要统计覆盖率的module的build.gradle中依赖这个gradle文件即可。
其中解释一点:

defaultPublishConfig "debug"

这个已经说明了,我们module默认对外暴露的就是debug,所以在我们的moduleA(一般也是app module)中该怎么依赖就怎么依赖,比如我的就是:

api project(path: ':app_jinggong_sdk')

就是这么朴素,不用加任何多余的指令,这就够了,信我!

3、Unable to read execution data file …/coverage.ec

有的人依赖后执行生成ec文件时会抛出这种问题,提示没有权限处理ec,或者读取失败,别想多的,调整jacoco的版本就完了,用这个,好使!

jacoco { 
   
  toolVersion = "0.8.2"
}

4、ec文件保存目录

网上介绍的都是使用这个路径:

"/mnt/sdcard/coverage.ec"

不知道你们是否好使,我用这个路径直接被拒绝,保存文件时报错,告诉我我没权限往这个目录下写文件,悲催!那就换一个路径呗,通过这行代码,将文件保存到files目录下:

getContext().getFilesDir().getPath() + "/coverage.ec"

亲测好使,另外有一点,当我们多次测试生成ec文件时,我通过Android Studio中Device File Explorer查看files目录下coverage.ec的创建时间,一直是上一次的,刚开始我以为是缓存问题,浏览器没来得及更新,但是后来偶然发现,我天真了,如果你也碰到这种情况,把你的手机拔了,再连上去,再看看,是不是马上刷新了,是不是你上次操作生成的文件!

5、executionData路径错误

网上的博客都是这么写的:

executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

其实我们要理解这个是什么意思,executionData指的是jacoco要执行解析ec文件的目录,那么我们应该以你项目执行createDebugCoverageReport任务生成的目录为主,而不是固定这种写法,懂了吗?比如在我的环境下执行createDebugCoverageReport命令后,coverage.ec文件生成的路径如图所示:
在这里插入图片描述
所以我的路径是:

executionData = files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec")

所以当我们理解了这是什么含义的时候,我们可以对示例作出相应的修改。

二、实践出真知

在列出了上面的几个坑,相信之前被困扰的同学应该知道自己的问题在哪儿了,这里附上一个完整版的实现代码,这里直接以多模块为例,如果你只有app一个module,修改相应代码就可以了,这也避免了同样的东西讲两遍。

以下列出的这几个文件和网上其他的一样,可以直接拿过来用,这里其实用的是监听我们的主Activity,这个一般是咱们app的首页MainActivity,不要把它理解为启动Activity,做的就是一件事,当这个Activity执行onDestroy方法的时候通知Instrumentation生成ec文件,所以你不想根据这种思路来走完全没有问题,实现一个工具类,在你想要执行生成ec文件的时候调用即可,道理一样,看你的使用场景和需求,废话不多说。

1、FinishListener

public interface FinishListener { 
   
  void onActivityFinished();
  void dumpIntermediateCoverage(String filePath);
}

2、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();
  }
}

3、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);
}
}
}

其实这个里面的代码有很多是可以优化的,我这里没有做深究,毕竟不是核心,你们可以自行处理。

4、在app模块下新建一个jacoco.gradle文件

这个jacoco.gradle文件,是提供给app模块build.gradle使用的,负责依赖jacoco插件,指定jacoco版本号,并且创建一个生成报告的任务,具体代码如下所示:

apply plugin: 'jacoco'
jacoco { 

toolVersion = "0.8.2"
}
//源代码路径,你有多少个module,你就在这写多少个路径
def coverageSourceDirs = [
'../app/src/main/java',
'../app_jinggong_sdk/src/main/java',
'../app_jinggong_store/src/main/java',
'../app_jinggong_flutter/src/main/java',
'../app_jinggong_libcore/src/main/java',
]
//class文件路径,就是我上面提到的class路径,看你的工程class生成路径是什么,替换我的就行
def coverageClassDirs = [
'../app/build/intermediates/javac/debug/classes',
'../app_jinggong_sdk/build/intermediates/javac/debug/classes',
'../app_jinggong_store/build/intermediates/javac/debug/classes',
'../app_jinggong_flutter/build/intermediates/javac/debug/classes',
'../app_jinggong_libcore/build/intermediates/javac/debug/classes',
]
//这个就是具体解析ec文件的任务,会根据我们指定的class路径、源码路径、ec路径进行解析输出
task jacocoTestReport(type: JacocoReport) { 

group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports { 

xml.enabled = true
html.enabled = true
}
classDirectories = files(files(coverageClassDirs).files.collect { 

fileTree(dir: it,
// 过滤不需要统计的class文件
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
})
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec")
doFirst { 

//遍历class路径下的所有文件,替换字符
coverageClassDirs.each { 
 path ->
new File(path).eachFileRecurse { 
 file ->
if (file.name.contains('$$')) { 

file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
}

然后在你的app的build.gradle文件中依赖这个jacoco.gradle,下面我给出一个通用的示例:

apply plugin: 'com.android.application'
apply from: 'jacoco.gradle'
android { 

compileSdkVersion 28
defaultConfig { 

applicationId "com.zhangyan.test"
minSdkVersion 19
targetSdkVersion 28
versionCode 1200000
versionName "2.0.0"
ndk { 

abiFilters "armeabi" //暂时支持模拟器,上线前移除x86
}
multiDexEnabled true
}
buildTypes { 

release { 

shrinkResources true
minifyEnabled true //混淆开关 遇到线上类找不到先将混淆去掉 对比是否是混淆问题
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug { 

/**打开覆盖率统计开关*/
testCoverageEnabled = true
signingConfig signingConfigs.BeikeConfig
debuggable true
shrinkResources false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies { 

implementation fileTree(dir: 'libs', include: ['*.jar'])
//主业务SDK
api project(path: ':app_jinggong_sdk')
}

5、在依赖的Library模块中添加依赖

看到app的build.gradle中我依赖了一个业务module:

//主业务SDK
api project(path: ':app_jinggong_sdk')

所以当我需要统计子module中的代码覆盖率的时候,我的子module也要作出相应的改造,具体jacoco-config.gradle参考上面第一章节第二小节,我将它放在工程根目录下:
在这里插入图片描述
这时候打开我的app_jinggong_sdk模块的build.gradle文件,添加代码:

apply plugin: 'com.android.library'
apply from: '../gradleCommon/jacoco-config.gradle'

具体的依赖都在我们的jacoco-config.gradle中,这样我们的module工程也打开了统计代码的开关,能够进行代码覆盖率的统计。有多少个依赖的子module,你就在那些子module的build.gradle文件中都添加这个jacoco-config.gradle文件依赖就好了。

6、配置AndroidManifest.xml

在app模块的AndroidManifest.xml添加一些配置,配置我们上面添加的InstrumentedActivity和JacocoInstrumentation。

//添加所需的权限
<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
android:name="com.zhangyan.test.MyApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...省略你自己其他的配置和页面...
<activity
android:name=".jacoco.InstrumentedActivity"
android:label="InstrumentationActivity" />
</application>
<instrumentation
android:name=".jacoco.JacocoInstrumentation"
android:handleProfiling="true"
android:label="CoverageInstrumentation"
android:targetPackage="com.zhangyan.test" />

配置完后,会发现targetPackage=”com.zhangyan.test”显红,但是没关系,不用管它,targetPackage对应的是应用的包名。

7、生成测试报告

1.installDebug

首先我们通过命令行安装app。
在这里插入图片描述
选择你的app -> Tasks -> install -> installDebug,安装app到你的手机上。

2.命令行启动

adb shell am instrument com.zhangyan.test/com.zhangyan.test.jacoco.JacocoInstrumentation

通过上述命令行启动app,adb shell am instrument 应用包名/Instrumentation完整路径名。

3.点击测试

这个时候你可以操作你的app,对你想进行代码覆盖率检测的地方,进入到对应的页面,点击对应的按钮,触发对应的逻辑,你现在所操作的都会被记录下来,在生成的coverage.ec文件中都能体现出来。当你点击完了,根据我们之前设置的逻辑,当我们MainActivity执行onDestroy方法时才会通知JacocoInstrumentation生成coverage.ec文件,我们可以按返回键退出MainActivity返回桌面,生成coverage.ec文件可能需要一点时间哦(取决于你点击测试页面多少,测试越多,生成文件越大,所需时间可能多一点)

然后在Android Studio的Device File Explore中,找到data/data/包名/files/coverage.ec文件,右键保存到桌面(随你喜欢)备用

4.createDebugCoverageReport

这个命令正常存在的路径是
在这里插入图片描述
双击它,会执行创建覆盖率报告的命令,等待它执行完,这个会生成一个covera.ec文件,但是这个不是我们最终需要分析的,我们需要分析的是我们刚才手动点击保存到桌面的那个。
在这里插入图片描述
等它执行完后,会在这个路径下生成一个coverage.ec文件,不用想,删掉它!然后把桌面的那个coverage.ec文件拷贝到这个路径下(当然coverage.ec文件拷贝到哪个路径都可以改,你的jacoco.gradle中执行的executionData对应的路径也得配套修改)

5.jacocoTestReport

在这里插入图片描述
找到这个路径,双击执行这个任务,会生成我们最终所需要代码覆盖率报告,执行完后,我们可以在这个目录下找到它

app/build/reports/jacoco/jacocoTestReport/html/index.html

在文件夹下双击打开就能看到我们的代码覆盖率报告

8、分析报告

以我项目实际运行结果为例,打开index.html后,首先会展示的是所有目录的整体覆盖率
在这里插入图片描述
点进去看一个
在这里插入图片描述
以页面的结果看看
在这里插入图片描述
绿色的就是代码执行到了,红色的就是代码没有执行到,我们可以根据这个来完善我们的测试逻辑,做到提交之前代码覆盖率尽可能百分百,不要漏过任何没测试的逻辑。

好了,到这里完整的Android + jacoco集成使用方式讲完了,对于今天想用的同学应该是一盏指路明灯吧,有问题欢迎留言!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/162125.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(2)
blank

相关推荐

  • 4G演进之路:FDD还是TDD?[通俗易懂]

    4G演进之路:FDD还是TDD?[通俗易懂]达到更高频谱利用率、覆盖率,同时保证多媒体应用的QoS服务质量,已经成为第四代蜂窝4G网络的挑战和目标。在4G系统里,有许多关于物理层和多接入以提高频谱利用率方面的研究,以支持高达100Mbps甚至更高的数据传输速率。例如,正交频分多址OFDMA、MIMO天线,以及跨层资源优化,被认为是4G系统中的核心技术,并同时在频率选择的衰落信道中提供高可靠通信。  另一方面,4G系统双工方式的选择,是F

  • verilog变长移位寄存器「建议收藏」

    verilog变长移位寄存器「建议收藏」verilog变长移位寄存器普通的移位寄存器可以按如下设置,下面是一个一次移动一位的移位寄存器。moduleshift_stationary(clk,dat,ctrl,shift_out);inputclk;input[15:0]dat;input[1:0]ctrl;outputreg[15:0]shift_out;always@(posedgeclk)begincase(ctrl)2’b00:begin

  • RedisClient 安装「建议收藏」

    RedisClient 安装「建议收藏」RedisClient安装下载RedisClient下载地址:https://github.com/caoxinyu/RedisClient安装双击,配置解压目录,并进行解压解压后文件,如图双击redisclient-win32.x86.2.0.exe,即可运行,运行如图…

    2022年10月12日
  • phpstorm2021激活码失效【在线注册码/序列号/破解码】

    phpstorm2021激活码失效【在线注册码/序列号/破解码】,https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

  • phpstorm激活码2021 64位_通用破解码

    phpstorm激活码2021 64位_通用破解码,https://javaforall.cn/100143.html。详细ieda激活码不妨到全栈程序员必看教程网一起来了解一下吧!

  • 浅谈@RequestMapping @ResponseBody 和 @RequestBody 注解的用法与区别

    浅谈@RequestMapping @ResponseBody 和 @RequestBody 注解的用法与区别博主说:首先,大家在使用SSM框架进行web开发的时候,经常会在Ctrl层遇到@RequestMapping、@ResponseBody以及@RequestBody这三个参数,博主就以自己在项目开发中总结的一些知识点浅谈一下三者之间微妙的关系。1.@RequestMapping国际惯例先介绍什么是@RequestMapping,@RequestMapping是一个用来处理请求地址映射的注解,可用

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号