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

时间:2023-03-08 22:59:03
jacoco统计Android手工测试覆盖率并自动上报服务器

改进了几个点

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

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

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

1. 新增覆盖率代码

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

2. git apply patch

为了不影响工程代码,我这里用git apply patch的方式应用的上面的覆盖率代码

首先git commit上面的覆盖率代码

然后git log查看commit

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

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

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

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

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

执行成功后的输出:

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

3. 服务器生成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/目录下的覆盖率报告拷贝到展现的位置

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