> 本文章是我上一篇文章的升级版本,详见地址:https://www.cnblogs.com/xiaoluosun/p/7234606.html

## 为什么要做这个?
1. 辛辛苦苦写了几百条测试用例,想知道这些用例的覆盖率能达到多少?
2. 勤勤恳恳验证好几天,也没啥bug了,可不可以上线?有没有漏测的功能点?
3. 多人协同下测试,想了解团队每个人的测试进度、已覆盖功能点、验证过的设备机型和手机系统等等。

## 数据采集和上报
既然要做覆盖率分析,数据的采集非常重要,除了JaCoCo生成的.ec文件之外,还需要拿到额外一些信息,如被测设备系统版本、系统机型、App的版本、用户唯一标识(UID)、被测环境等等。

什么时候触发数据的上报呢?这个机制很重要,如果设计的不合理,覆盖率数据可能会有问题。

最早使用的上报策略是:加在监听设备按键的位置,如果点击设备back键或者home键把App置于后台,则上报覆盖率数据。
这种设计肯定是会有问题的,因为有些时候手机设备用完就扔那了,根本没有置于后台,第二天可能才会继续使用,这时候上报的数据就变成了第二天的。还可能用完之后杀死了App,根据就不会上报,覆盖率数据造成丢失;

所以优化后的上报策略是:定时上报,每一分钟上报一次,只要App进程活着就会上报。
那怎么解决用完就杀死App的问题呢?解决办法是App重新启动后查找ec文件目录,如果有上次的记录就上报,这样就不会丢覆盖率数据了。

#### 生成覆盖率文件
```java

/**
* Created by sun on 17/7/4.
*/

public class JacocoUtils {
static String TAG = "JacocoUtils";

//ec文件的路径
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";

/**
* 生成ec文件
*
* @param isNew 是否重新创建ec文件
*/
public static void generateEcFile(boolean isNew) {
// String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec";
Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
try {
if (isNew && mCoverageFilePath.exists()) {
Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件");
mCoverageFilePath.delete();
}
if (!mCoverageFilePath.exists()) {
mCoverageFilePath.createNewFile();
}
out = new FileOutputStream(mCoverageFilePath.getPath(), true);

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));

// ec文件自动上报到服务器
UploadService uploadService = new UploadService(mCoverageFilePath);
uploadService.start();
} catch (Exception e) {
Log.e(TAG, "generateEcFile: " + e.getMessage());
} finally {
if (out == null)
return;
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
```

#### 采集到想要的数据上传服务器
```java

/**
* Created by sun on 17/7/4.
*/

public class UploadService extends Thread{

private File file;
public UploadService(File file) {
this.file = file;
}

public void run() {
Log.i("UploadService", "initCoverageInfo");
// 当前时间
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar cal = Calendar.getInstance();
String create_time = format.format(cal.getTime()).substring(0,19);

// 系统版本
String os_version = DeviceUtils.getSystemVersion();

// 系统机型
String device_name = DeviceUtils.getDeviceType();

// 应用版本
String app_version = DeviceUtils.getAppVersionName(LuojiLabApplication.getInstance());

// 应用版本
String uid = String.valueOf(AccountUtils.getInstance().getUserId());

// 环境
String context = String.valueOf(BuildConfig.SERVER_ENVIRONMENT);

Map<String, String> params = new HashMap<String, String>();
params.put("os_version", os_version);
params.put("device_name", device_name);
params.put("app_version", app_version);
params.put("uid", uid);
params.put("context", context);
params.put("create_time", create_time);

try {
post("https://xxx.com/coverage/uploadec", params, file);
} catch (IOException e) {
e.printStackTrace();
}

}

/**
* 通过拼接的方式构造请求内容,实现参数传输以及文件传输
*
* @param url Service net address
* @param params text content
* @param files pictures
* @return String result of Service response
* @throws IOException
*/
public static String post(String url, Map<String, String> params, File files)
throws IOException {
String BOUNDARY = java.util.UUID.randomUUID().toString();
String PREFIX = "--", LINEND = "\r\n";
String MULTIPART_FROM_DATA = "multipart/form-data";
String CHARSET = "UTF-8";

Log.i("UploadService", url);
URL uri = new URL(url);
HttpURLConnection conn = (HttpURLConnection) uri.openConnection();
conn.setReadTimeout(10 * 1000); // 缓存的最长时间
conn.setDoInput(true);// 允许输入
conn.setDoOutput(true);// 允许输出
conn.setUseCaches(false); // 不允许使用缓存
conn.setRequestMethod("POST");
conn.setRequestProperty("connection", "keep-alive");
conn.setRequestProperty("Charsert", "UTF-8");
conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY);

// 首先组拼文本类型的参数
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(PREFIX);
sb.append(BOUNDARY);
sb.append(LINEND);
sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINEND);
sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND);
sb.append("Content-Transfer-Encoding: 8bit" + LINEND);
sb.append(LINEND);
sb.append(entry.getValue());
sb.append(LINEND);
}

DataOutputStream outStream = new DataOutputStream(conn.getOutputStream());
outStream.write(sb.toString().getBytes());
// 发送文件数据
if (files != null) {
StringBuilder sb1 = new StringBuilder();
sb1.append(PREFIX);
sb1.append(BOUNDARY);
sb1.append(LINEND);
sb1.append("Content-Disposition: form-data; name=\"uploadfile\"; filename=\""
+ files.getName() + "\"" + LINEND);
sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND);
sb1.append(LINEND);
outStream.write(sb1.toString().getBytes());

InputStream is = new FileInputStream(files);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}

is.close();
outStream.write(LINEND.getBytes());
}

// 请求结束标志
byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes();
outStream.write(end_data);
outStream.flush();
// 得到响应码
int res = conn.getResponseCode();
Log.i("UploadService", String.valueOf(res));
InputStream in = conn.getInputStream();
StringBuilder sb2 = new StringBuilder();
if (res == 200) {
int ch;
while ((ch = in.read()) != -1) {
sb2.append((char) ch);
}
}
outStream.close();
conn.disconnect();
return sb2.toString();
}
}

```

#### 上报数据的定时器
```java
/**
* 定时器,每分钟调用一次生成覆盖率方法
*
*/
public boolean timer() {
JacocoUtils.generateEcFile(true);
}
```

#### 启用JaCoCo

##### 安装plugin

```gradle
apply plugin: 'jacoco'

jacoco {
toolVersion = '0.7.9'
}
```

##### 启用覆盖率开关
此处是在debug时启用覆盖率的收集

Android 9.0以上版本因为限制私有API的集成,所以如果打开了开关,9.0以上系统使用App时会有系统级toast提示“Detected problems with API compatibility”,但不影响功能。
```gradle
buildTypes {
debug {
testCoverageEnabled = true
}
}
```

#### 分析源码和二进制,生成覆盖率报告
执行命令生成
```bash
./gradlew jacocoTestReport
```

这块做的时候遇到三个问题。
第一个问题是App已经拆成组件了,每个主要模块都是一个可独立编译的业务组件。如果按照之前的方法只能统计到主工程的覆盖率,业务组件的覆盖率统计不到。
解决办法是是先拿到所有业务组件的名称和路径(我们在settings.gradle里有定义),然后循环添加成一个list,files方法支持list当做二进制目录传入。

第二个问题是部分业务组件是用Kotlin开发的,所以要同时兼容Java和Kotlin两种编程语言。
解决办法跟问题一的一样,files同时支持Kotlin的二进制目录传入。

第三个问题是覆盖率数据是碎片式的,每天会有上万个覆盖率文件生成,之前只做过单个文件的覆盖率计算,如何批量计算覆盖率文件?
解决办法是使用fileTree方法的includes,用正则表达式*号,批量计算特定目录下符合规则的所有.ec文件。
```
executionData = fileTree(dir: "$buildDir", includes: [
"outputs/code-coverage/connected/*coverage.ec"
])
```

完整代码
```gradle
task jacocoTestReport(type: JacocoReport) {
def lineList = new File(project.rootDir.toString() + '/settings.gradle').readLines()
def coverageCompName = []
for (i in lineList) {
if (!i.isEmpty() && i.contains('include')) {
coverageCompName.add(project.rootDir.toString() + '/' + i.split(':')[1].replace("'", '') + '/')
}
}

def coverageSourceCompName = []
for (i in lineList) {
if (!i.isEmpty() && i.contains('include')) {
coverageSourceCompName.add('../' + i.split(':')[1].replace("'", '') + '/')
}
}

reports {
xml.enabled = true
html.enabled = true
}
def fileFilter = ['**/R*.class',
'**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class',
'**/*Binding*.class',
'**/*BR*.class'
]

def coverageSourceDirs = []
for (i in coverageSourceCompName) {
def sourceDir = i + 'src/main/java'
coverageSourceDirs.add(sourceDir)
}

def coverageClassDirs = []
for (i in coverageCompName) {
def classDir = fileTree(dir: i + 'build/intermediates/classes/release', excludes: fileFilter)
coverageClassDirs.add(classDir)
}

def coverageKotlinClassDirs = []
for (i in coverageCompName) {
def classKotlinDir = fileTree(dir: i + 'build/tmp/kotlin-classes/release', excludes: fileFilter)
coverageKotlinClassDirs.add(classKotlinDir)
}

classDirectories = files(coverageClassDirs, coverageKotlinClassDirs)
sourceDirectories = files(coverageSourceDirs)
// executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
executionData = fileTree(dir: "$buildDir", includes: [
"outputs/code-coverage/connected/*coverage.ec"
])

doFirst {
new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}

```

## 数据分析和处理

待补充。。。。

#### 应用环境的覆盖率分析
![](/uploads/photo/2019/4372fc8b-816d-419d-8f01-0474f0756915.png!large)

#### 设备系统的覆盖率分析
![](/uploads/photo/2019/7bc728bb-0973-4ec8-87e0-6d323d1ce0fa.png!large)

#### 用户UID的覆盖率分析
![](/uploads/photo/2019/f1fb4d66-288c-46fd-881d-16a5e6cb6602.png!large)

#### 应用版本的覆盖率分析
![](/uploads/photo/2019/98e43b2f-49ba-4b85-83af-1192185c56a9.png!large)

基于JaCoCo的Android测试覆盖率统计(二)的更多相关文章

  1. vivo 基于 JaCoCo 的测试覆盖率设计与实践

    作者:vivo 互联网服务器团队- Xu Shen 本文主要介绍vivo内部研发平台使用JaCoCo实现测试覆盖率的实践,包括JaCoCo原理介绍以及在实践过程中遇到的新增代码覆盖率统计问题和频繁发布 ...

  2. jacoco统计Android手工测试覆盖率并自动上报服务器

    改进了几个点 1. 不用借助Instrumentation启动,正常启动即可: 2. 测试代码不用push到主分支,主分支代码拉到本地后用git apply patch方式合并覆盖率代码: 3. 测试 ...

  3. Sonar + Jacoco,强悍的UT, IT 双覆盖率统计(转)

    以前做统计代码测试覆盖,一般用Cobertura.以前统计测试覆盖率,一般只算Unit Test,或者闭上眼睛把Unit Test和Integration Test一起算. 但是,我们已经过了迷信UT ...

  4. 使用Cobertura统计JUnit测试覆盖率

    这是一个JavaProject,关于Cobertura的用法详见代码注释 首先是应用代码(即被测试的代码) package com.jadyer.service; public class Calcu ...

  5. Android高手进阶教程(二十八)之---Android ViewPager控件的使用(基于ViewPager的横向相册)!!!

      分类: Android高手进阶 Android基础教程 2012-09-14 18:10 29759人阅读 评论(35) 收藏 举报 android相册layoutobjectclassloade ...

  6. Android消息推送(二)--基于MQTT协议实现的推送功能

    国内的Android设备,不能稳定的使用Google GCM(Google Cloud Messageing)消息推送服务. 1. 国内的Android设备,基本上从操作系统底层开始就去掉了Googl ...

  7. Android测试(二):Android测试基础

    原文地址:https://developer.android.com/training/testing/fundamentals.html 用户在不同的级别上与你的应用产生交互.从按下按钮到将信息下载 ...

  8. 统计 Django 项目的测试覆盖率

    作者:HelloGitHub-追梦人物 文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试.现在 ...

  9. Android测试提升效率批处理脚本(二)

    前言: 前面放出过一次批处理,本次再放出一些比较有用的批处理(获得当前包名.查看APP签名信息等),好长时没来写博客了,简单化,请看正文,更多脚本尽请期待~~~(不定期) 目录 1.[手机录屏(安卓4 ...

随机推荐

  1. docker search/pull 报错

    docker报错 Get https://registry-1.docker.io/v2/: x509: certificate has expired or is not yet valid 这种错 ...

  2. 用.NET Core实现一个类似于饿了吗的简易拆红包功能

      需求说明 以前很讨厌点外卖的我,最近中午经常点外卖,因为确实很方便,提前点好餐,算准时间,就可以在下班的时候吃上饭,然后省下的那些时间就可以在中午的时候多休息一下了. 点餐结束后,会有一个好友分享 ...

  3. Linux 勿卸载软件,所有命令不能用了咋办

    1. 一次有趣的事 有个做技术的(不说什么岗位,容易被人喷,谁都有失手的时候),在公司的业务测试环境的机器,卸载了一个软件rpm -e --nodeps filesystem* , 导致机器所有的命令 ...

  4. 并发编程-concurrent指南-Lock-可重入锁(ReentrantLock)

    可重入和不可重入的概念是这样的:当一个线程获得了当前实例的锁,并进入方法A,这个线程在没有释放这把锁的时候,能否再次进入方法A呢? 可重入锁:可以再次进入方法A,就是说在释放锁前此线程可以再次进入方法 ...

  5. 2018.11.2 2018NOIP冲刺之最短公共父串

    很有意思的一个题 试题描述 给定字符串A和字符串B,要求找一个最短的字符串,使得字符串A和B均是它的子序列. 输入 输入包含两行,每行一个字符串,分别表示字符串A和字符串B.(串的长度不超过30) 输 ...

  6. Linux嵌入式GDB调试环境搭建

    ======================= 我的环境 ==========================PC 端: CPU:x86_64, 系统:Ubuntu,IP:172.16.2.212开发 ...

  7. asp.net core系列 68 Filter管道过滤器

    一.概述 本篇详细了解一下asp.net core filters,filter叫"筛选器"也叫"过滤器",是请求处理管道中的特定阶段之前或之后运行代码.fil ...

  8. 托管堆和垃圾回收(GC)

    一.基础 首先,为了深入了解垃圾回收(GC),我们要了解一些基础知识: CLR:Common Language Runtime,即公共语言运行时,是一个可由多种面向CLR的编程语言使用的"运 ...

  9. Touch Bar 废物利用系列 | 在触控栏上显示 Dock 应用图标

    都说 Intel 第八代 CPU 对比上代是牙膏不小心挤多了,而配备第八代 CPU 的 MacBook Pro,只有 Touch Bar 版本,虽然贵了一点,但就一个字 -- 买! 收到电脑后,兴冲冲 ...

  10. android_SurfaceView 画图

    有这样一种view类,可以让人在其上面画动画,画图片,它的全名叫做surfaceview.名称就包含两层意思,一层是surface,一层是view.前一层提供一个面可以让人画画,后一层是个view,可 ...