移动测试基础 jacoco 统计 Android 手工测试覆盖率并自动上报服务器

xiaoluosun · 2017年07月25日 · 最后由 剪烛 回复于 2018年11月22日 · 1634 次阅读
本帖已被设为精华帖!

改进了几个点

  1. 不用借助Instrumentation启动,正常启动即可;

  2. 测试代码不用push到主分支,主分支代码拉到本地后用git apply patch方式合并覆盖率代码;

  3. 测试完成后,连按两次back键把app置于后台,并自动上报覆盖率文件到服务器;

新增覆盖率代码

src下新建一个test package,放入下面两个测试类

import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
* 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();
}
}
}
}

上传ec文件和设计信息到服务器

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

import android.util.Log;

import com.x.x.x.LuojiLabApplication;
import com.x.x.x.DeviceUtils;

/**
* 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 context = "";

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("create_time", create_time);

try {
post("http://x.x.x.x:8888/importCodeCoverage!upload", 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();
}
}

在build.gradle新增

apply plugin: 'jacoco'

jacoco {
toolVersion = '0.7.9'
}
buildTypes {
release {
     // 在release下统计覆盖率信息
testCoverageEnabled = true
}
}

最重要的一行代码,加在监听设备按键的地方,如果连续2次点击设备back键,app已置于后台,则调用生成覆盖率方法。

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
....

JacocoUtils.generateEcFile(true);
}

}

git apply patch

为了不影响工程代码,我这里用git apply patch的方式应用的上面的覆盖率代码
首先git commit上面的覆盖率代码
然后git log查看commit

我提交覆盖率代码的commit是最近的一次,然后拿到上一次的commit,并生成.patch文件,-o是输出目录

git format-patch 0e4c................... -o ~/Documents/jk/script/

把.patch文件拷贝到自动打包的机器,
使用Jenkins自动打包,拉取最新代码后,在编译前Execute shell自动执行下面的命令,把覆盖率文件应用到工程内

git apply --reject ~/Documents/jk/script/0001-patch.patch

执行成功后的输出:

然后就可以安装包含统计覆盖率代码的测试包了,测试完后后,连按两次back键生成覆盖率文件并上传到服务器。

服务器生成jacoco覆盖率报告

在服务器我也拉了一个Android工程,专门用于生成报告
主要在build.gradle新增

def coverageSourceDirs = [
'../app/src/main/java'
]

task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
classDirectories = fileTree(
dir: './build/intermediates/classes/debug',
excludes: ['**/R*.class',
'**/*$InjectAdapter.class',
'**/
*$ModuleAdapter.class',
'**/*$ViewInjector*.class'
])
sourceDirectories = files(coverageSourceDirs)
executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")

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

然后设备上传ec文件到Android工程的$buildDir/outputs/code-coverage/connected目录下,并依次执行

gradle createDebugCoverageReport
gradle jacocoTestReport

最后把$buildDir/reports/jacoco/目录下的覆盖率报告拷贝到展现的位置

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 10 条回复 时间 点赞
思寒_seveniruby 将本帖设为了精华贴 07月25日 19:42

赞,我们也是类似的实现方式,想到一起了~

git apply 其实不用 commit 也可以,只需要把想修改的部分通过 git add 加到暂存区,然后 git diff --cached > xxx.patch 就可以生成 patch 文件了。

另外,分享下 patch 的坑。我们用 patch 这种方式用了3个月左右,期间由于被 patch 的部分前后代码有改动导致 patch 失败的出现概率大概是50%,导致耗费了非常多时间来修正 patch 文件。最后还是选择了 debug 包固定开覆盖率,release 固定关覆盖率的方式取代 patch 这种方式。

http 请求部分建议用 okhttp 库,现在的写法感觉比较累赘。

手动收藏

先mark一下

mark以下,这个比较不错

陈恒捷 回复

确实,git apply的坑已经遇到了,经常有冲突。后来又优化了下,只在build.gradle和一个activity里加两行代码。冲突的可能性已经很低了。

http请求部分直接用的以前的代码,没有优化,有时间改成okhttp试下。

如果打包的频率提高,比如两个小时一个包,会影响测试覆盖率的统计结果吧

@xiaoluosun @chenhengjie123
请问这个方案适用于组件化版本吗,可有应用过?我现在遇到的情况是用task jacocoTestReport将coverage.ec转换为报告时出错了.

aar包可以覆盖吗

请问一下,我按照流程做下来,在AS中直接运行app,是生成时会找不到jacoco的类的,但是新建一个InstrumentationTest,然后通过Instrumentation运行,是可以正常生成覆盖率文件的,请问这个是正常的吗?是怎么实现不通过Instrumentation运行,手动运行就可以生成覆盖率文件的呢?
多谢。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册