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

news2025/1/9 23:54:29

背景

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

Android由于是本地安装包,只能使用offline模式:
在测试前先对文件进行插桩,然后生成插过桩的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.zip

3.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
            //引入jacoco
            isTestCoverageEnabled = true
            zipAlignEnabled(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;
    }

    @Override
    public 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() {

    }

    @Override
    public 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();
    }

    @Override
    public 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);
    }

    @Override
    public void onActivityFinished() {
        if (LOGD) {
            Log.e(TAG, "onActivityFinished()");
        }
        if (mCoverage) {
            Log.e(TAG, "onActivityFinished mCoverage true");
            generateCoverageReport();
        }
        finish(Activity.RESULT_OK, mResults);
    }

    @Override
    public void dumpIntermediateCoverage(String filePath) {
        // TODO Auto-generated method stub
        if (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-->
    <instrumentation
        android: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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2040628.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java 面试 PDF 资料整理

“尊贵的求知者&#xff0c;作者特此献上精心编纂的Java面试宝典PDF&#xff0c;这份资料凝聚了无数面试精华与实战经验&#xff0c;是通往Java技术殿堂的钥匙。若您渴望在Java编程的求职之路上稳健前行&#xff0c;只需轻轻一点&#xff0c;完成这象征支持与认可的一键三联&am…

Dubbo源码深度解析(五)

上一篇博客主要讲服务提供方服务的发布&#xff0c;以及Netty是如何启动的&#xff0c;客户端发过来的请求&#xff0c;会经过哪些处理器&#xff0c;以及补充之前没讲完的SPI机制等等。这篇博客将会接着继续讲&#xff0c;在看这篇博客之前&#xff0c;请先看上一篇博客&#…

VTK—vtkStructuredGrid提取维度面数据

1.在VTK自带的vtkStructuredGrid数据文件combq.bin和combxyz.bin 2.文件读取代码如下&#xff1a; //读取数据文件Create(vtkMultiBlockPLOT3DReader, reader);reader->SetXYZFileName("G:/Temp/vtkTest/combxyz.bin");reader->SetQFileName("G:/Temp/v…

Vitis AI 基本认知(训练过程)

目录 1. 目的 2. TensorBoard 2.1 In TensorFlow 2.2.1 安装 TensorBoard 2.2.2 导入必要的库 2.2.3 初始化 2.2.4 记录数据 2.2.5 启动 TensorBoard 2.2.6 刷新间隔 2.2 In PyTorch 3. 训练周期 Epoch 3.1 Epoch 3.2 Batch 3.3 Iteration 4. 总结 1. 目的 介绍…

传奇游戏发布渠道

传奇游戏发布渠道 回答&#xff1a;游戏发布平台|手机游戏发布平台 传奇游戏发布渠道作为游戏开发商直接控制的信息传播途径&#xff0c;其安全性自然有着较高的保障。首先&#xff0c;渠道通常会采用先进的加密技术和安全协议来保护数据传输过程中的安全&#xff0c;防止信息…

Centos 7 升级GCC时遇到 mirrorlist.centos.org; Unknown error“

问题描述 在执行如下操作的时候&#xff0c; yum install devtoolset-9-gcc devtoolset-9-gcc-c devtoolset-9-binutils 出现&#xff1a; 14: curl#6 - "Could not resolve host: mirrorlist.centos.org; Unknown error" 网上搜索了一下&#xff0c;原因是 mir…

redis集合若干记录

无序集合 redis通常使用字典结构保存集合数据&#xff0c;字典健存储集合元素&#xff0c;字典值为空。如果一个集合全为整数&#xff0c;使用字典就有点浪费了&#xff0c;redis使用intset保存。 插入元素到intset中 获取插入元素编码&#xff0c;如果插入元素编码级别高于int…

Chapter 36 PySpark数据计算

欢迎大家订阅【Python从入门到精通】专栏&#xff0c;一起探索Python的无限可能&#xff01; 文章目录 前言一、map算子二、flatMap算子三、reduceByKey算子四、filter算子五、distinct算子六、sortBy算子七、综合案例 前言 在大数据处理的时代&#xff0c;Apache Spark以其高…

猫头虎 分享:Python库 Pygame 的简介、安装、用法详解入门教程

猫头虎 分享&#xff1a;Python库 Pygame 的简介、安装、用法详解入门教程 &#x1f63a; 摘要&#xff1a;今天&#xff0c;猫头虎将带大家深入了解Python中常用的Pygame库。Pygame是开发2D游戏和多媒体应用的首选工具之一。在本文中&#xff0c;我们将从安装Pygame、了解Pyg…

深入学习零拷贝

在学习中遇到了一个问题就是什么是零拷贝&#xff0c;因此学习之后以此来记录一下。 零拷贝、直接I/O、异步I/O等&#xff0c;优化的目的就是为了提高系统的吞吐量&#xff0c;减少访问磁盘次数。访问磁盘的速度会比读写内存会慢十倍以上。因此就需要提高它的读写速度。 什么…

uniapp自定义请求头信息header

添加请求头&#xff1a;uniapp自定义请求头信息header 代码

Java性能优化之并发编程:深入解析与实战技巧

在Java应用程序的性能优化中&#xff0c;并发编程是一个关键领域。通过合理使用并发编程技术&#xff0c;可以充分利用多核CPU的计算能力&#xff0c;提高程序的执行效率。本文将深入探讨Java并发编程的优化策略&#xff0c;并提供一些实用的代码示例和实战技巧。 1. 线程与同…

Linux Day1 系统编程和文件操作

系统编程内容 文件I/O (输入/输出): 1&#xff09;使用标准库函数如fopen, fclose, fread, fwrite, fgetc, fputc, fgets, fprintf, fscanf等进行文件操作。 2&#xff09;使用open, close, read, write等系统调用来实现底层文件操作。 进程管理: 1&#xff09;使用fork, e…

力扣 3152. 特殊数字Ⅱ

题目描述 queries二维数组是nums数组待判断的索引区间&#xff08;左闭右闭&#xff09;。需要判断每个索引区间中的nums相邻元素奇偶性是否不同&#xff0c;如果都不同则该索引区间的搜索结果为True&#xff0c;否则为False。 暴力推演&#xff1a;也是我最开始的思路 遍历q…

招聘技术研发类岗位,HR会考察候选人哪些方面?

技术研发团队在当下的企业视为发展的核心&#xff0c;对于企业长期发展和市场竞争力至关重要&#xff0c;作为HR&#xff0c;如何选拔技术研发岗位的人才&#xff0c;也是难度较大的工作。 作为应聘者来说&#xff0c;同样应该主动去了解HR是如何考察技术性人才&#xff0c;以…

使用docker部署rabbitmq集群

部署环境准备 192.168.81.128 rabbitmq-1 192.168.81.129 rabbitmq-2 192.168.81.130 rabbitmq-3 首先创建挂载目录&#xff08;三个节点都创建&#xff09; systemctl stop firewalld && setenforce 0 关闭防火墙和selinux mkdir /data/rabbitmq -p cd /da…

加和分数、训练、测试

一、加和所有alignment的分数 1、路线图中 2、l_i只与token有关&#xff0c;有一个专门训练的网络&#xff1b;h_i变化只与null有关 3、distribution生成的概率不受路径影响&#xff0c;只要到达位置概率就是一样的 4、计算alignment分数的总和 &#xff08;1&#xff09;αi…

Word转html并移植到web项目

1.打开对应word文件 建议使用web视图查看文档 这样可以提前预览转转成html样式 2.如果有图片修改图片大小及格式 在web视图下&#xff0c;把图片调制适当大小&#xff0c;不然导出的html可能图片较小 3.点击另存为 4.选择网页格式&#xff0c;同时将后缀修改为html(默认是h…

从springBoot框架服务器上下载文件 自定义一个启动器

在springboot框架中下载服务器存储的图片&#xff1a; 1&#xff09;springboot默认访问放行的目录只有static&#xff0c;在static目录下存放图片资源 2&#xff09;编译后的static目录中有一个1.png 2.5)编写控制器&#xff1a; Controller //RequestMapping("/upload&q…

如何在 Linux 内核中高效使用链表:原理与实践

文章目录 前言一、Linux内核链表源码分析1.链表的初始化1. 静态初始化宏 LIST_HEAD_INIT(name)宏 LIST_HEAD(name) 2. 动态初始化函数 INIT_LIST_HEAD(struct list_head *list) 对比总结2.链表的添加list_add 函数的定义函数参数内部实现__list_add 函数 list_add 的功能总结使…