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 xx.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
相关文章:

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

乌龟对对碰在线版
爆肝两天使用vue开发了一个在线版的乌龟对对碰小游戏之幸运对对碰。没有找到合适的乌龟素材,现在使用小兔子代替。 体验地址:幸运对对碰 | 乌龟对对碰小游戏 之前的python版本的乌龟对对碰:写文章-CSDN博客 乌龟对对碰-幸运对对碰...
如何更改select option边框颜色和选中的颜色
<!doctype html> <html> <head> <meta charset"utf-8"> <title>如何更改select option边框颜色和选中的颜色</title> </head><style>ul{border: 1px solid #000000;width: 500px;height: auto;background-color: aq…...

6. 数据结构—串的匹配算法
1.BF算法(暴力算法) //模式匹配(暴力算法) int Index(SString S,SString T){int i1,j1;while(i<S.length&&j<T.length){if(S[i]T[i]){i;j;}else{ii-j2; //最开始匹配的位置的后一个j1; //从头匹配 }}if(j>T.length)return i-T.length;return return 0…...

九大服务架构性能优化方式
来源:九大服务架构性能优化方式 目录 性能优化九大方式: 缓存 使用什么样的缓存 缓存常见问题 缓存淘汰 缓存数据一致性 并行化处理 批量化处理 数据压缩合并 无锁化 顺序写 分片化 避免请求 池化 异步处理 总结 最近做了一些服务性能优…...

【RabbitMQ】 相关概念 + 工作模式
本文将介绍一些MQ中常见的概念,同时也会简单实现一下RabbitMQ的工作流程。 MQ概念 Message Queue消息队列。是用来存储消息的队列,多用于分布式系统之间的通信。 系统间调用通常有:同步通信和异步通信。MQ就是在异步通信的时候使用的。 同…...
嵌入式学习 ——(Linux高级编程——进程)
目录 一、进程的含义 二、进程和程序的区别 三、进程的作用 四、进程的状态 五、进程的调度与上下文切换 六、查询进程相关命令 七、fork()函数 八、getpid()和getppid()函数 九、面试题解析: 十、应用场合及测试 一、进程的含义 进程指正在运行的程序&a…...

C++练习备忘录
1. 保留两位小数输出格式 #include <iostream> #include <iomanip> using namespace std; int main() {double S 0;S (15 25) * 20 / 2;cout << fixed << setprecision(2) << S;return 0; }2. 设置输出宽度 #include <iostream> #inclu…...
改善工作流
快捷键管理器 打开Editor->Shortcuts查看和编辑Unity中的快捷键 示例 ShiftSpace 窗口最大化 P 选择预制体 进入预制体编辑模式 单一检视窗口 选择组件,选择Properties打开一个窗口,显示组件信息;切换对象,窗口信息不会改变…...

迭代器失效
一、什么是迭代器失效 迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T* 。因此迭代器失效,实际就是迭代器底层对应指针所指…...
@RequestParam @RequestBody @PathVariable 这三个注解对应的前端使用vue的http请求时不同的调用方式
1. RequestParam 用途:用于提取请求参数,常见于GET请求或表单提交。 Vue HTTP 请求示例: // 使用axios发送GET请求 axios.get(/api/users, { params: { id: 1, name: John } }); 2. RequestBody 用途:用于提取请求体…...
SQL - 索引
索引本质上是数据库引擎用来快速查找数据的数据结构,可以显著提高查询的性能,为了加快运行较慢的查询。创建索引 默认索引 create index 索引名 on 表名 (列名); 通过对列名进行创建索引,在查询的时候,数据库就能通过索引找到匹配…...
Oracle23ai新特性FOR LOOP循环控制结构增强
在Oracle数据库中,FOR LOOP是一种常用的循环控制结构,它允许你重复执行一系列语句固定次数或直到满足特定条件为止。然而,标准的Oracle PL/SQL中的FOR LOOP主要用于遍历集合(如数组或游标的结果集),而不是像…...

DHU OJ 二维数组
思路及代码 #include<iostream> using namespace std; int main(){ //input 多组 //input M,N int 1< <20 //input M 行 N 列 数据 //initialize listint M, N;while (cin >> M >> N){int list[M][N];for (int i 0; i < M-1; i){for (int j 0; j…...

UDP/TCP --- Socket编程
本篇将使用 Linux 中的系统调用来实现模拟 TCP 和 UDP 的通信过程,其中只对 UDP 和 TCP 进行了简单的介绍,本篇主要实现的是代码,至于 UDP 和 TCP 的详细讲解将会在之后的文章中给出。 本篇给出的 tcp 和 udp 的代码中的 echo 都是测试连接是…...

【C语言】最详细的单链表(两遍包会!)
🦄个人主页:小米里的大麦-CSDN博客 🎏所属专栏:C语言数据结构_小米里的大麦的博客-CSDN博客 🎁代码托管:黄灿灿/数据结构 (gitee.com) ⚙️操作环境:Visual Studio 2022 目录 一、前言 二、单链表的概念 1. 单链表的特点 2. 单链表的基本…...

QT:VS2019 CMake编译CEF
CEF介绍 CEF作为一个基于Chromium的开源Web浏览器控件,为第三方应用提供了强大的嵌入浏览器支持。其多平台支持、HTML5特性、自定义能力以及多进程架构等特性,使得CEF在浏览器开发、桌面应用、开发工具以及自动化测试等领域得到了广泛应用。 多平台支持…...
day31(8/19)——静态文件共享、playbook
目录 一、ansible模块 script模块 copy模块 使用command模块下载 nfs-utils rpcbind 在被控制的主机上添加static目录,并创建test文件 command模块 service模块 二、playbook 三、playbook编排vsftpd 1、安装 2、卸载 3、启动服务 4、修改配置文件设置不…...
白骑士的C#教学实战项目篇 4.4 游戏开发
系列目录 上一篇:白骑士的C#教学实战项目篇 4.3 Web开发 在这一部分,我们将探索如何使用 Unity 和 C# 开发游戏。游戏开发结合了编程、图形设计和创意,既充满挑战又充满乐趣。通过这一节的学习,您将了解游戏引擎的基础知识&#…...

在Vue工程中开发页面时,发现页面垂直方向出现两个滚动条的处理
在Vue工程中开发页面时,发现页面垂直方向出现两个滚动条 最近在开发页面时,发现页面多了两个滚动条,如图: 原因: 当一个页面的内容高度大于屏幕的高度时就会出现滚动条。一般情况下当一个页面高度大于屏幕高度时&a…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...

Linux相关概念和易错知识点(42)(TCP的连接管理、可靠性、面临复杂网络的处理)
目录 1.TCP的连接管理机制(1)三次握手①握手过程②对握手过程的理解 (2)四次挥手(3)握手和挥手的触发(4)状态切换①挥手过程中状态的切换②握手过程中状态的切换 2.TCP的可靠性&…...

抖音增长新引擎:品融电商,一站式全案代运营领跑者
抖音增长新引擎:品融电商,一站式全案代运营领跑者 在抖音这个日活超7亿的流量汪洋中,品牌如何破浪前行?自建团队成本高、效果难控;碎片化运营又难成合力——这正是许多企业面临的增长困局。品融电商以「抖音全案代运营…...
拉力测试cuda pytorch 把 4070显卡拉满
import torch import timedef stress_test_gpu(matrix_size16384, duration300):"""对GPU进行压力测试,通过持续的矩阵乘法来最大化GPU利用率参数:matrix_size: 矩阵维度大小,增大可提高计算复杂度duration: 测试持续时间(秒&…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)
本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...