当前位置: 首页 > news >正文

Android+Jacoco+code-diff全量、增量覆盖率生成实战

背景

主要是记录下Android项目使用jacoco生成代码覆盖率的实战流程,目前已完成全量覆盖方案,仅使用jacoco就能实现;
由于我们的Android端是使用Java和kotlin语言,目前增量的方案code-diff仅针对Java代码,卡在kotlin文件的分析,仍在思考中。

Android由于是本地安装包,只能使用offline模式:

offline模式就是在测试之前先对文件进行插桩,生成插过桩的class或jar包,测试插过桩的class和jar包,生成覆盖率信息到文件,最后统一处理,生成报告。

在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。

使用场景

其实主要是基于两个痛点:

1、新功能测试和回归测试在手工测试的情况下,即便用例写的再怎么详细,也经常会有漏测的发生,这里一方面是因为现在大量互联网公司采用外包资源来做业务测试,而外包的工作质量无法有效评估,可能存在漏执行的情况,另外一方面是本身测试用例设计的不够完善导致没有覆盖到一些关键路径的代码分支,因此亟需一种可以度量手工测试完成后对代码覆盖情况的手段或者工具;

2、研发代码变更的影响范围难以精准评估,比如研发提交一个MR,这个MR到底影响了多少用例,在没有精准测试能力的情况下是很难给出的,而做精准测试,最重要的一环就是代码用例的关系库维护,如何生成代码跟用例的关系,就需要用到代码覆盖率的采集和分析能力了;
引用简单两步实现 Jacoco+Android 代码覆盖率的接入!(最新最全版)

时机:
1.提测时-明确整个版本迭代的改动范围,测试范围,全量代码diff;
2.测试中-提交bug修复版本,明确问题,使用增量代码diff;
3.预发布-关注关键点,确保发布代码与测试代码一致,全量代码diff;

覆盖率对测试提升:
1.能了解确认需求的实现逻辑,对技术细节查漏补缺;
2.评估影响范围;
3.通过代码补充测试范围,优化测试用例;
4.加深系统实现的理解;
5.提前发现错误

项目环境

1.gradle插件版本
ANDROID_GRADLE_PLUGIN = "4.2.0"2.gradle依赖版本
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip3.android sdk版本
BUILD_TOOLS_VERSION = "28.0.3"
COMPILE_SDK = 31
TARGET_SDK = 31
MIN_SDK = 21

代码介入

1.在app模块下新建一个 jacoco.gradle

apply plugin: 'jacoco'
jacoco {toolVersion = "0.8.2"
}android {//在app引入的时候指定对应的变体 会将内容传递引用的工程,主要用于多模块使用defaultPublishConfig "debug"buildTypes {debug {/**打开覆盖率统计开关**/testCoverageEnabled = true}}
}
//源代码路径,你有多少个module,你就在这写多少个路径
//我这里是多模块的,需要将主要代码的模块写上
def coverageSourceDirs = ['../lib.xx/src/main/java','../lib.xx/src/main/java','../lib.xx/src/main/java','../lib.xx/src/main/java',......'/src/main/java','/src/mvp/java'
]//class文件路径,就是上面提到的class路径,看你的工程class生成路径是什么,替换一下就行
def coverageClassDirs = ['/lib.xx/build/intermediates/javac/debug/classes','/lib.xx/build/intermediates/javac/debug/classes','/lib.xx/build/intermediates/javac/debug/classes','/lib.xx/build/intermediates/javac/debug/classes','/app/build/intermediates/javac/debug/classes'......
]
//kotlin的classes文件
def kotlinClassDirs = ['/lib.xx/build/tmp/kotlin-classes/debug/','/lib.xx/build/tmp/kotlin-classes/debug/','/lib.xx/build/tmp/kotlin-classes/debug/','/lib.xx/build/tmp/kotlin-classes/debug/','/app/build/tmp/kotlin-classes/debug/'......
]//这个就是具体解析ec文件的任务,会根据我们指定的class路径、源码路径、ec路径进行解析输出
task jacocoTestReport(type: JacocoReport) {group = "Reporting"description = "Generate Jacoco coverage reports after running tests."reports {xml.enabled(true)html.enabled(true)}//设置class文件的路径classDirectories.setFrom(files(coverageClassDirs.collect{fileTree(dir: "$rootDir"+it,excludes: ['**/R*.class','**/*$InjectAdapter.class','**/*$ModuleAdapter.class','**/*$ViewInjector*.class'])}))classDirectories.setFrom(files(kotlinClassDirs.collect{fileTree(dir: "$rootDir"+it,excludes: ['**/R*.class','**/*$InjectAdapter.class','**/*$ModuleAdapter.class','**/*$ViewInjector*.class'])}))//设置源码文件的路径sourceDirectories.setFrom(files(coverageSourceDirs))
//设置ec文件executionData.setFrom(files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/coverage.ec"))doFirst {coverageClassDirs.each { path ->println("$rootDir" + path)new File("$rootDir" + path).eachFileRecurse { file ->if (file.name.contains('$$')) {file.renameTo(file.path.replace('$$', '$'))}}}}
}

2.在app模块下的build.gradle.kts引用jacoco.gradle,并在buildtype为debug下开启覆盖率的开关

apply(from = "jacoco.gradle")//引入jacoco
// 开发版本,可打开开发者模式getByName("debug") {isMinifyEnabled = false//引入jacocoisTestCoverageEnabled = truezipAlignEnabled(false)

3.定义采集覆盖率coverage.ec的方式,网上的方式都是通过监听主activity Destroy后收集,这里可以自己定义适合的方式,比如在项目新增按钮点击采集。参考网上的代码可以,直接用:

在app的代码新建jacoco目录
添加一下代码
在这里插入图片描述

FinishListener

package xx.app.jacoco;public interface FinishListener {void onActivityFinished();void dumpIntermediateCoverage(String filePath);
}

InstrumentedActivity

package xx.app.jacoco;import xx.Activity;public class InstrumentedActivity extends Activity {public FinishListener finishListener;public void setFinishListener(FinishListener finishListener) {this.finishListener = finishListener;}@Overridepublic void onDestroy() {if (this.finishListener != null) {finishListener.onActivityFinished();}super.onDestroy();}
}

JacocoInstrumentation

public class JacocoInstrumentation extends Instrumentation implements FinishListener {public static String TAG = "JacocoInstrumentation:";@SuppressLint("SdCardPath")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() {}@Overridepublic void onCreate(Bundle arguments) {Log.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()) {Log.e(TAG, "file del successs");} else {Log.e(TAG, "file del fail !");}}if (!file.exists()) {try {file.createNewFile();} catch (IOException e) {Log.e(TAG, "异常 : " + e);e.printStackTrace();}}if (arguments != null) {Log.e(TAG, "arguments不为空 : " + arguments);mCoverageFilePath = arguments.getString("coverageFile");Log.e(TAG, "mCoverageFilePath = " + mCoverageFilePath);}mIntent = new Intent(getTargetContext(), InstrumentedActivity.class);mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);start();}@Overridepublic void onStart() {Log.e(TAG, "onStart def");if (LOGD) {Log.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) {Log.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;Log.e(TAG, msg);mResults.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\nError: "+ msg);}@Overridepublic void onActivityFinished() {if (LOGD) {Log.e(TAG, "onActivityFinished()");}if (mCoverage) {Log.e(TAG, "onActivityFinished mCoverage true");generateCoverageReport();}finish(Activity.RESULT_OK, mResults);}@Overridepublic void dumpIntermediateCoverage(String filePath) {// TODO Auto-generated method stubif (LOGD) {Log.e(TAG, "Intermidate Dump Called with file name :" + filePath);}if (mCoverage) {if (!setCoverageFilePath(filePath)) {if (LOGD) {Log.e(TAG, "Unable to set the given file path:" + filePath + " as dump target.");}}generateCoverageReport();setCoverageFilePath(DEFAULT_COVERAGE_FILE_PATH);}}
}

配置AndroidManifest.xml

<!--引入jacoco--><activity android:name=".jacoco.InstrumentedActivity"android:label="InstrumentationActivity"/>
<!--引入jacoco--><instrumentationandroid:name=".jacoco.JacocoInstrumentation"android:handleProfiling="true"android:label="CoverageInstrumentation"android:targetPackage="包名" />

统计子module的覆盖率

因为很多Android项目肯定不只要app module,有很多子module提供使用,需要一起统计覆盖率
目前的做法是在jacoco.gradle 加上参数 defaultPublishConfig “debug”

android {//在app 引入的时候指定对应的变体 会将内容传递引用的工程,主要用于多模块使用defaultPublishConfig "debug"buildTypes {debug {/**打开覆盖率统计开关**/testCoverageEnabled = true}}
}

然后让子module去引用,这就需要修改子module的build.gradle,一行代码完成

//在子模块引入jacoco
apply(from = "../app/jacoco.gradle")

实战使用

1.通过命令行打debug安装包
installDebug 或者 gradlew app都行

2.通过instrument 启动app
安装完后先打开app再退出一下,不然启动不了

adb shell pm list instrumentation
//会看到以下信息
instrumentation:xx.app/.jacoco.JacocoInstrumentation (target=xx.app)
//然后复制启动
adb shell am instrument co.runner.app/.jacoco.JacocoInstrumentation

3.执行测试

4.完成测试后,在主页面退出app

5.通过Android stdio的device file explorer复制出coverage.ec
路径 /data/data/xx.app/files/coverage.ec

6.将coverage.ec复制到项目文件\app\build\outputs\code_coverage\debugAndroidTest\connected下,如没有的话新建

7.用命令jacocoTestReport生成报告,报名路径如下:
\app\build\reports\jacoco\jacocoTestReport\html
在这里插入图片描述

增量代码覆盖率

使用code-diff 和 jacoco二开

用code-diff获取两个commit之间的代码差异,然后生成json文件,使用jacoco二开的jar包通过
–diffCodeFiles 传入差异代码json文件,然后只生成差异代码文件的覆盖报告
在这里插入图片描述

在这里插入图片描述
总结:KT文件需要改造code-diff才能用,目前只能用于java,后续看看怎么修改。

引用下该作者的话,总结得很好,学习学习:

代码覆盖率100% 不代表没有bug。代码没有覆盖100% 一定有bug;
但是有可能你覆盖到80% 很轻松,往后增加5% 都费很大劲。那么我们可以去没有覆盖到的进行分析。不一定要做到代码100%全覆盖,尤其在功能测试阶段,代码100% 覆盖,会给大家增加很多的工作量,很有可能为了1%的覆盖率而耽误整体测试,得不偿失。
覆盖率是为了提升我们测试用例的覆盖度,检验我们测试用例设计的全面性,它有两面性,合理引入覆盖率,合理选择一定的阈值。

https://cloud.tencent.com.cn/developer/article/1801772

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 共享经济背景下校园、办公闲置物品交易平台-计算机毕设Java|springboot实战项目
  • 系统架构设计师 - 软件工程(2)
  • Mysql面试一
  • 【数据结构算法经典题目刨析(c语言)】使用栈实现队列(图文详解)
  • javaEE中自定义注解以及注解的解析
  • CSP部分模拟题题解
  • 探索sqlmap的奥秘:Python中的强大SQL注入检测工具
  • python实现K-means图像聚类
  • Kubernetes--命令行工具 kubectl
  • 参与团体标准的意义以及作用
  • 旋转图像(LeetCode)
  • docker 启动 mongo,redis,nacos.
  • 网络安全实训第三天(文件上传、SQL注入漏洞)
  • 用7EPhone云手机进行TikTok的矩阵运营
  • 自己对对抗性样本的实质的理解
  • 【108天】Java——《Head First Java》笔记(第1-4章)
  • Android框架之Volley
  • create-react-app项目添加less配置
  • Elasticsearch 参考指南(升级前重新索引)
  • HTTP 简介
  • idea + plantuml 画流程图
  • js中的正则表达式入门
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • MySQL几个简单SQL的优化
  • Mysql数据库的条件查询语句
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • 从输入URL到页面加载发生了什么
  • 多线程 start 和 run 方法到底有什么区别?
  • 将 Measurements 和 Units 应用到物理学
  • 蓝海存储开关机注意事项总结
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • ​RecSys 2022 | 面向人岗匹配的双向选择偏好建模
  • ​低代码平台的核心价值与优势
  • ​马来语翻译中文去哪比较好?
  • ​十个常见的 Python 脚本 (详细介绍 + 代码举例)
  • #define用法
  • #nginx配置案例
  • (6)添加vue-cookie
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (二)Linux——Linux常用指令
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (全部习题答案)研究生英语读写教程基础级教师用书PDF|| 研究生英语读写教程提高级教师用书PDF
  • (一)Java算法:二分查找
  • (自适应手机端)响应式新闻博客知识类pbootcms网站模板 自媒体运营博客网站源码下载
  • .gitignore文件设置了忽略但不生效
  • .libPaths()设置包加载目录
  • .net websocket 获取http登录的用户_如何解密浏览器的登录密码?获取浏览器内用户信息?...
  • .NET6使用MiniExcel根据数据源横向导出头部标题及数据
  • .NET框架设计—常被忽视的C#设计技巧
  • .NET设计模式(2):单件模式(Singleton Pattern)
  • /bin/rm: 参数列表过长"的解决办法
  • /dev/VolGroup00/LogVol00:unexpected inconsistency;run fsck manually
  • /dev下添加设备节点的方法步骤(通过device_create)