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

第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..".将覆盖率数据文件下载到本地目录。
     下载到覆盖率数据文件后,就可以根据需要生成全量和增量覆盖率报告,检测测试情况,排查漏测问题补充测试用例。

相关文章:

  • 用 pytorch 从零开始创建大语言模型(零):汇总
  • 轻松迁移 Elasticsearch 数据:如何将自建索引导出并导入到另一个实例
  • 通过 Executors 创建线程池
  • Java基础编程练习第35题-可实现多种排序的Book类(PTA练习题)
  • 第十六届蓝桥杯模拟二
  • PowerBI 条形图,解决数据标签在条形内部看不清的问题
  • DeepSeek R1 本地部署指南 (2) - macOS 本地部署
  • 初级:控制流程面试题精讲
  • LabVIEW液压传动系统教学仿真平台
  • 2025_0321_生活记录
  • 【蓝桥杯速成】| 9.回溯升级
  • SvelteKit 最新中文文档教程(8)—— 部署 Node 服务端
  • STM32——基本定时器
  • PHP PSR(PHP Standards Recommendations)介绍
  • Vue 3 + TypeScript 实现视频播放与字幕功能:集成西瓜播放器 XGPlayer
  • vscode + latex workshop + sumatraPDF
  • 破局 MySQL 死锁:深入理解锁机制与高效解决方案
  • 日事清在敏捷开发中的实战应用:SCRUM框架下可视化项目管理+高效沟通机制驱动灵活迭代
  • 画出ConcurrentHashMap 1.8的put流程图,记住CAS和synchronized的结合
  • Powershell、Windows Shell、CMD 的区别与联系
  • 以军向也门3个港口的居民发布撤离令
  • 微软将在全球裁员6000人,目标之一为减少管理层
  • 习近平同巴西总统卢拉共同会见记者
  • 比特币挖矿公司GRYP股价涨超171%:将与特朗普儿子创设的公司合并
  • 花2万多在海底捞办婚礼,连锁餐企要抢酒楼的婚宴生意?
  • 上汽享道出行完成13亿元C轮融资,已启动港股IPO计划