基于 Android JaCoCo 针对手工测试的代码变更覆盖率方案

2023-11-27 14:10

本文主要是介绍基于 Android JaCoCo 针对手工测试的代码变更覆盖率方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

      • 1、需求背景
      • 2、工具选型
      • 3、技术选型
        • 3.1 On-The-Fly(在线插桩)
        • 3.2 Offliine(离线插桩)
        • 3.3 结论
      • 4、手工获取测试覆盖率
        • 4.1 添加代码
        • 4.2 新建一个 jacoco.gradle 文件
        • 4.3 在依赖的 Library 模块中添加依赖
        • 4.4 配置 AndroidManifest.xml
        • 4.5 生成测试报告
        • 4.6 分析报告
      • 5、在上述方案上可再改进
      • 6、参考

1、需求背景

  • 随着业务与需求的增长,回归测试的范围越来越大,测试人员的压力也日益增加,但即使通过测试同学的保障,线上仍然会存在回归不到位或测试遗漏的地方导致出现线上故障。
  • 因此我们需要通过类似 JaCoCo 的集成测试覆盖率统计框架,来衡量测试人员的回归范围是否精准、测试场景是否遗漏;保障上线的代码都已经经过测试人员验证。
  • 针对这一点,我们提出了 Android 测试覆盖率统计工具, 借此来提升测试人员精准测试的能力,借助覆盖率数据补充测试遗漏的测试用例。

2、工具选型

  • Android App 开发主流语言就是 Java 语言,而 Java 常用覆盖率工具为 JaCoCoEmmaCobertura
类别JaCoCoEmmaCobertura
原理使用 ASM 修改字节码可以修改 Jar 文件、class 文件字节码文件基于 Jcoverage 和 ASM 框架对 class 插桩
覆盖粒度方法、类、行、分支、指令、圈行、块、方法、类行、分支
插桩on-the-fly 和 offlineon-the-fly 和 offlineoffline
缺点不支持 JDK8关闭服务器才能获取覆盖率报告
性能较快较快
  • 根据上述的一些特点,最终选择 JaCoCo 作为测试覆盖率统计工具。

3、技术选型

image

  • 众所周知,获取覆盖率数据的前提条件是需要完成代码的插桩工作。
  • 而针对字节码的插桩方式,可分为两种:
3.1 On-The-Fly(在线插桩)
  • JVM 中 通过 -javaagent 参数指定特定的 jar 文件启动 Instrumentation 的代理程序;
  • 代理程序在每装载一个 class 文件前判断是否已经转换修改了该文件,如果没有则需要将探针插入 class 文件中。
  • 代码覆盖率就可以在 JVM 执行代码的时候实时获取;
  • 优点:无需提前进行字节码插桩,无需考虑 classpath 的设置。测试覆盖率分析可以在 JVM 执行测试代码的过程中完成。
3.2 Offliine(离线插桩)
  • 在测试之前先对字节码进行插桩,生成插过桩的 class 文件或者 jar 包,执行插过桩的 class 文件或者 jar 包之后,会生成覆盖率信息到文件,最后统一对覆盖率信息进行处理,并生成报告。
  • Offlline 模式适用于以下场景:
  • 运行环境不支持 Java agent,部署环境不允许设置 JVM 参数;
  • 字节码需要被转换成其他虚拟机字节码,如 Android Dalvik VM 动态修改字节码过程中和其他 agent 冲突;
  • 无法自定义用户加载类
3.3 结论
  • Android 项目只能使用 JaCoCo 的离线插桩方式。为什么呢?
  • 一般运行在服务器 Java 程序的插桩可以在加载 class 文件进行,运用 Java Agent 的机制,可以理解成“实时插桩”。但是因为 Android 覆盖率的特殊性,导致 Android 系统破坏了 JaCoCo 这种便利性,原因有两个:
  • (1)Android 虚拟机不同与服务器上的 JVM,它所支持的字节码必须经过处理支持 Android Dalvik 等专用虚拟机,所以插桩必须在处理之前完成,即离线插桩模式。
  • (2)Android 虚拟机没有配置 JVM 配置项的机制,所以应用启动时没有机会直接配置 dump 输出方式。
  • 所以通过上述这些最终确定了 Android JaCoCo 覆盖率是采用离线插桩的方式进行。

4、手工获取测试覆盖率

  • 为了不修改开发的核心代码,我们可以采用通过 instrumentation 调起被测 App,在 instrumentation activity 退出时增加覆盖率的统计(不修改核心源代码)。
4.1 添加代码
  • 在不修改 Android 源码的情况下,在 src/main/java 里面新增一个 JaCoCo 目录 里面存放 3 个文件:FinishListenerInstrumentedActivityJacocoInstrumentation
  • FinishListener.java 代码:
public interface FinishListener {void onActivityFinished();void dumpIntermediateCoverage(String filePath);
}
  • InstrumentedActivity.java 代码:
public class InstrumentedActivity extends MainActivity {public FinishListener finishListener;public void setFinishListener(FinishListener finishListener) {this.finishListener = finishListener;}@Overridepublic void onDestroy() {if (this.finishListener != null) {finishListener.onActivityFinished();}super.onDestroy();}
}
  • JacocoInstrumentation.java 代码:
public class JacocoInstrumentation extends Instrumentation implements FinishListener {public static String TAG = "JacocoInstrumentation:";private static String DEFAULT_COVERAGE_FILE_PATH = "";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);}}
}
4.2 新建一个 jacoco.gradle 文件
  • 在项目根目录下新建一个 jacoco.gradle 文件,这个文件提供给各个模块使用。
apply plugin: 'jacoco'jacoco {toolVersion = "0.8.7"
}// 源代码路径,你有多少个module,你就在这写多少个路径
def coverageSourceDirs = ['../app/src/main/java',
]// class文件路径,就是我上面提到的class路径,看你的工程class生成路径是什么,替换我的就行
def coverageClassDirs = ['../app/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.setFrom(files(files(coverageClassDirs).files.collect {fileTree(dir: it,// 过滤不需要统计的class文件excludes: ['**/R*.class','**/*$InjectAdapter.class','**/*$ModuleAdapter.class','**/*$ViewInjector*.class'])}))sourceDirectories.setFrom(files(coverageSourceDirs))executionData.setFrom(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('$$', '$'))}}}}
}
4.3 在依赖的 Library 模块中添加依赖
  • 在您的 app 或子模块的 build.gradle 文件中依赖这个 jacoco.gradle
apply from: '../jacoco.gradle'
4.4 配置 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"package="您的包名">// 添加所需的权限<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...><activityandroid:name=".jacoco.InstrumentedActivity"android:label="InstrumentationActivity" /></application><instrumentationandroid:name=".jacoco.JacocoInstrumentation"android:handleProfiling="true"android:label="CoverageInstrumentation"android:targetPackage="您的包名" /></manifest>
4.5 生成测试报告
  • (1)installDebug
  • 首先我们通过命令行安装 app
    image)
  • 选择您的 app -> Tasks -> install -> installDebug,安装 app 到您的手机上。
  • (2)命令行启动
adb shell am instrument 您的包名/您的包名.jacoco.JacocoInstrumentation
  • (3)点击测试
  • 这个时候你可以操作您的 app,对您想进行代码覆盖率检测的地方,进入到对应的页面,点击对应的按钮,触发对应的逻辑,你现在所操作的都会被记录下来,在生成的 coverage.ec 文件中都能体现出来。当您点击完了,根据我们之前设置的逻辑,当我们 MainActivity 执行 onDestroy() 方法时才会通知 JacocoInstrumentation 生成 coverage.ec 文件,我们可以按返回键退出 MainActivity 返回桌面,生成 coverage.ec 文件可能需要一点时间哦(取决于您点击测试页面多少,测试越多,生成文件越大,所需时间可能多一点)
  • 然后在 Android Studio的Device File Explore 中,找到 d ata/data/包名/files/coverage.ec 文件,右键保存到桌面备用。
  • (4)createDebugCoverageReport
    image
  • 选择您的 app -> Tasks -> verification -> createDebugCoverageReport,然后执行。
  • (5)jacocoTestReport
    image
  • 找到这个路径,双击执行这个任务,会生成我们最终所需要代码覆盖率报告,执行完后,我们可以在这个目录下找到它
app/build/reports/jacoco/jacocoTestReport/html/index.html
  • 在文件夹下双击打开就能看到我们的代码覆盖率报告
4.6 分析报告

image

5、在上述方案上可再改进

  • 上述的步骤最终可以通过一个或者多个 .sh 脚本去执行,从而降低复杂程度 。
  • 上述方法都是基于 Android 全量代码手工测试的覆盖率统计,需要改进的是最终变成 Android 增量代码手工测试的覆盖率统计。

6、参考

  • https://zhuanlan.zhihu.com/p/88332971
  • https://tech.meituan.com/2017/06/16/android-jacoco-practace.html
  • https://blog.csdn.net/woshizisezise/article/details/115638097
  • https://tech.kujiale.com/androidjing-zhun-ce-shi-tan-suo-ce-shi-fu-gai-lu-tong-ji/
  • https://juejin.cn/post/6920029313316159502

这篇关于基于 Android JaCoCo 针对手工测试的代码变更覆盖率方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/427823

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Nginx设置连接超时并进行测试的方法步骤

《Nginx设置连接超时并进行测试的方法步骤》在高并发场景下,如果客户端与服务器的连接长时间未响应,会占用大量的系统资源,影响其他正常请求的处理效率,为了解决这个问题,可以通过设置Nginx的连接... 目录设置连接超时目的操作步骤测试连接超时测试方法:总结:设置连接超时目的设置客户端与服务器之间的连接

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav