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

第2.2节 Android Jacoco插件覆盖率采集

        JaCoCo(Java Code Coverage)是一款开源的代码覆盖率分析工具,适用于Java和Android项目。它通过插桩技术统计测试过程中代码的执行情况,生成可视化报告,帮助开发者评估测试用例的有效性。在github上开源的项目: GitHub - jacoco/jacoco: :microscope: Java Code Coverage Library ,是针对服务端的,而移动端的jacoco插件暂时没有开源,可以参考: The JaCoCo Plugin ,使用最多的版本是0.8.7,你也可以尝试使用最新版本。

2.2.1 Jacoco插件的接入

      将要接入Jacoco插件的一个Android应用,或是从github上下载一个Demo来进行测试,不过网上的Demo可能因为gradle或是其他包的版本不兼容最新的版本,需要先进行处理一下,能打包后再进行接入jacoco插件。现在我以一个简单的Android计算器的Demo做一个jacoco接入的演示,早期github上的项目地址是 https://github.com/FlamingJay/AndroidCalculator.git,后来被删除了,后面我将上传到我的github上供大家学习。
  • build.gradle中添加jacoco插件
在app下的build.gradle文件中添加对jacoco的引用,如下所示:
plugins {
    id 'com.android.application'
    id 'jacoco'
}

jacoco {
    toolVersion = "0.8.7" // 选择合适的版本
}

注意:此处使用的是0.8.7版本,这个版本比较稳定,你也可以使用最新版本。

  • 打开覆盖率采集开关
  • android {
        ...
        buildTypes {
            release {
                minifyEnabled false
                testCoverageEnabled = true
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
            debug {
                testCoverageEnabled = true
            }
        }
        ...
        }
  • 一般在debug包下进行覆盖率的测试,打开testCoverageEnabled = true, 构建项目的时候就能对代码进行插桩,采集覆盖率数据。
  • release包有代码混淆,覆盖率报告渲染的时候,无法正确对应到类的源码,所以要对release包进行测试时,需要关掉代码混淆。
     通过上面的配置,打包后的App就可以采集覆盖率数据,记录用户的具体操作覆盖。注意:此时的覆盖率数据存在于内存中,要想拿到覆盖率数据,必须人为地将覆盖率数据写入到文件中。

2.2.2 覆盖率数据采集

      由于覆盖率数据内容存在于手机内存中,当App退出后,内存中的数据将被清空。而我们要进行覆盖率测试的时候,必须要拿到覆盖率数据文件,下面我们将借助于jacoco将覆盖率数据从内容写入到文件中,代码如下:
package com.example.calculator.utils;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

public class GenerateECFile {
    public static String TAG = "GenerateECFile:";
    private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
    private static String partStr="coverage";
    
    public static List<File> getDeletePath(Context context) {
        List<File> files = new ArrayList<>();
        File sdDir = new File(context.getFilesDir().getPath());
        String[] list = sdDir.list();
        if (list != null) {
            for(int i=0;i<list.length;i++){
                if(list[i].contains(partStr)){
                    files.add(new File(sdDir.getPath() + "/" + list[i]));
                }
            }
        }
        return files;
    }

    /**
     * 删除覆率数据文件
     * @param context
     */
    public static void deleteCoverageFiles(Context context){
        List<File> files = getDeletePath(context);
        if (files!= null && files.size() > 0) {
            for(File file:files){
                Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件path:+"+ file.getPath());
                // FileUtils.deleteFile(file);
                boolean result = file.delete();
                if (!result && file.exists()) {
                    try{
                        throw new IOException("Failed to delete " + file.getAbsolutePath());
                    }catch(IOException e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void onJacocoCreate(Context context) {
        Log.d(TAG, "onJacocoCreate");
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
        Calendar cal = Calendar.getInstance();
        String create_time = format.format(cal.getTime()).substring(0,19);
        // 获取packagemanager的实例
        PackageManager packageManager = context.getPackageManager();
        // getPackageName()是你当前类的包名,0代表是获取版本信息
        try{
            //删除原来的覆盖率数据文件
            deleteCoverageFiles(context);
            //生成新覆盖率数据文件名
            PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(),0);
            String app_version = packInfo.versionName;
            DEFAULT_COVERAGE_FILE_PATH = context.getFilesDir().getPath() + "/coverage"+"-"+app_version+"-"+create_time+".ec";
        }catch(PackageManager.NameNotFoundException e){
            e.printStackTrace();
            Log.d(TAG,"找不到包名"+e);
        }
    }

    /**
     * 生成覆盖率数据文件
     * @param context
     */
    public static void generateCoverageFile(Context context) {
        OutputStream out = null;
        try {
            //如果文件不存在,创建覆盖率数据文件
            File file = new File(DEFAULT_COVERAGE_FILE_PATH);
            if(!file.exists()){
                try{
                    file.createNewFile();
                }catch (IOException e){
                    Log.d(TAG,"新建文件异常:"+e);
                    e.printStackTrace();}
            }
            //将内存中的覆盖率数据写入到文件中
            out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, true);
            Object agent = Class.forName("org.jacoco.agent.rt.RT")
                    .getMethod("getAgent", new Class[0]).invoke(null);

            out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
                    .invoke(agent, false));

            Log.d(TAG, "生成覆盖率数据文件:"+DEFAULT_COVERAGE_FILE_PATH);
        } catch (Exception e) {
            Log.d(TAG, e.toString(), e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

说明:

  • 本代码借助于jacoco.agent将手机内存中的覆盖率数据文件写入到文件中;
  • 默认文件路径是本应用的files文件夹,由于现在高版本的android系统不允许访问手机存储,只能存储到App的本身空间中,所以在AndroidManifest.xml文件中要添加:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  • 代码中有初始化,清除原来覆盖率文件的函数,也有生成覆盖率文件的函数,只要在适合的时机调用即可。
  • 文件命名:coverage-app版本号-日期-时间.ec 如:coverage-1.0-2025-02-11-10_50_03.ec。

2.2.3 何时生成覆盖率文件?

通过专门的类,可以将手机中的覆盖率数据写入到应用的空间中,保存成覆盖率文件。现在存在一个问题,什么时候保存覆盖率数据文件?由于覆盖率数据存在于内存中,一旦应用退出 ,数据将被清除。分析一下app的生命周期,不难发现:
  • 在app进入前端时,清除原来的覆盖率数据文件,开始采集覆盖率数据;
  • 在app进入后台时,生成覆盖率数据文件。
这样交互进行比较合适。如果你的应用中有生命周期控制类,在相应的函数中引用上面的覆盖率生成函数即可,如果没有,请按如下方法,在MainActivity中的onCreate函数中添加生命周期控制函数,如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
   .....
    // 注册LifecycleObserver
    ProcessLifecycleOwner.get().getLifecycle().addObserver(new LifecycleObserver() {
        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        public void onMoveToForeground() {
            //清除原来的覆盖率数据文件
            GenerateECFile.onJacocoCreate(MainActivity.this);
            // 应用从后台移动到前台时调用
            Log.i(TAG,"App moved to foreground");
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        public void onMoveToBackground() {
            //开始生成覆盖率数据文件
            GenerateECFile.generateCoverageFile(MainActivity.this);
            // 应用从前台移动到后台时调用
            Log.i(TAG,"App moved to background");
        }
    });
    ....
    }

 添加了以上操作,就可以在App的生命周期中采集覆盖率数据,并写入到文件中。

       将通过上面修改的app打包,安装到手机上进行测试。打开计算器,随便进行一些操作或是执行一些测试用例,然后再将app置于后台,注意不要杀死App。此时会将前面操作的覆盖率数据写入数据文件,置到后台一会儿后,再杀死应用,就可以拿覆盖率数据文件了。

2.2.4 下载覆盖率数据文件

    根据设置覆盖率数据文件会生成在手机下面的位置:/data/data/应用包名/files,但是正常的手机系统由于安全设置,是无法下载下来的:
此时,在Android Studio点击右侧的Device Manager,找到连接的手机设备,单击设备最右侧的按钮,选择"Open in Device Explorer",就可以打开手机的文件系统,如下所示:
     找到对应的覆盖率数据文件的位置,如:/data/data/com.example.calculator/files/,就可以看到覆盖率数据文件,右击文件,选择"save as..".将覆盖率数据文件下载到本地目录。
     下载到覆盖率数据文件后,就可以根据需要生成全量和增量覆盖率报告,检测测试情况,排查漏测问题补充测试用例。

http://www.kler.cn/a/595082.html

相关文章:

  • Claude 3.7 Sonnet 根据UI设计稿生成前端html页面
  • 11--华为防火墙Easy-IP实现原理与配置全解:从“翻译官“到“流量导演“的奇幻之旅(包你看一遍全记住)
  • 数仓工具—Hive语法之不同纬度聚合
  • 高并发库存系统是否适合使用 ORM(Hibernate / MyBatis)
  • Java线程池深度解析:从使用到调优
  • 基于PySide6的CATIA自动化工具开发实战——空几何体批量清理系统
  • 在 Ubuntu 中用 Docker 安装 RAGFlow
  • 深入理解Java的 JIT(即时编译器)
  • 语法: bit_clear(var, bit)
  • PostgreSQL 数据库中导入大量数据
  • 【NoC仿真器(nirgam noxim)搭建】自用笔记
  • Spring Boot集成Redis并设置密码后报错: NOAUTH Authentication required
  • 【颠覆性缓存架构】Caffeine双引擎缓存实战:CPU和内存双优化,命中率提升到92%,内存减少75%
  • 本地生活服务APP开发,市场发展全新商业机遇
  • LDAP从入门到实战:环境部署与配置指南(上)
  • Linux环境变量:深入解析与实用指南
  • # [RPA] 使用八爪鱼进行高效网页数据采集
  • 看盘细节系列 篇五:盘中突然拉升涨停的特征分析
  • MySQL错误 “duplicate entry ‘1‘ for key ‘PRIMARY‘“ 解决方案
  • 云原生分布式存储:数据洪流中的时空折叠艺术