代码改变世界

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

2017-07-25 15:56  xiaoluosun  阅读(2557)  评论(1编辑  收藏  举报

改进了几个点

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

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

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

 

1. 新增覆盖率代码

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

 1 import android.util.Log;
 2 
 3 import java.io.File;
 4 import java.io.FileOutputStream;
 5 import java.io.IOException;
 6 import java.io.OutputStream;
 7 
 8 /**
 9  * Created by sun on 17/7/4.
10  */
11 
12 public class JacocoUtils {
13     static String TAG = "JacocoUtils";
14 
15     //ec文件的路径
16     private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";
17 
18     /**
19      * 生成ec文件
20      *
21      * @param isNew 是否重新创建ec文件
22      */
23     public static void generateEcFile(boolean isNew) {
24 //        String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec";
25         Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
26         OutputStream out = null;
27         File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
28         try {
29             if (isNew && mCoverageFilePath.exists()) {
30                 Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件");
31                 mCoverageFilePath.delete();
32             }
33             if (!mCoverageFilePath.exists()) {
34                 mCoverageFilePath.createNewFile();
35             }
36             out = new FileOutputStream(mCoverageFilePath.getPath(), true);
37 
38             Object agent = Class.forName("org.jacoco.agent.rt.RT")
39                     .getMethod("getAgent")
40                     .invoke(null);
41 
42             out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class)
43                     .invoke(agent, false));
44 
45             // ec文件自动上报到服务器
46             UploadService uploadService = new UploadService(mCoverageFilePath);
47             uploadService.start();
48         } catch (Exception e) {
49             Log.e(TAG, "generateEcFile: " + e.getMessage());
50         } finally {
51             if (out == null)
52                 return;
53             try {
54                 out.close();
55             } catch (IOException e) {
56                 e.printStackTrace();
57             }
58         }
59     }
60 }

 

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

  1 import java.io.DataOutputStream;
  2 import java.io.File;
  3 import java.io.FileInputStream;
  4 import java.io.IOException;
  5 import java.io.InputStream;
  6 import java.net.HttpURLConnection;
  7 import java.net.URL;
  8 import java.text.SimpleDateFormat;
  9 import java.util.Calendar;
 10 import java.util.HashMap;
 11 import java.util.Map;
 12 
 13 import android.util.Log;
 14 
 15 import com.x.x.x.LuojiLabApplication;
 16 import com.x.x.x.DeviceUtils;
 17 
 18 /**
 19  * Created by sun on 17/7/4.
 20  */
 21 
 22 public class UploadService extends Thread{
 23 
 24     private File file;
 25     public UploadService(File file) {
 26         this.file = file;
 27     }
 28 
 29     public void run() {
 30         Log.i("UploadService", "initCoverageInfo");
 31         // 当前时间
 32         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 33         Calendar cal = Calendar.getInstance();
 34         String create_time = format.format(cal.getTime()).substring(0,19);
 35 
 36         // 系统版本
 37         String os_version = DeviceUtils.getSystemVersion();
 38 
 39         // 系统机型
 40         String device_name = DeviceUtils.getDeviceType();
 41 
 42         // 应用版本
 43         String app_version = DeviceUtils.getAppVersionName(LuojiLabApplication.getInstance());
 44 
 45         // 环境
 46         String context = "";
 47 
 48         Map<String, String> params = new HashMap<String, String>();
 49         params.put("os_version", os_version);
 50         params.put("device_name", device_name);
 51         params.put("app_version", app_version);
 52         params.put("create_time", create_time);
 53 
 54         try {
 55             post("http://x.x.x.x:8888/importCodeCoverage!upload", params, file);
 56         } catch (IOException e) {
 57             e.printStackTrace();
 58         }
 59 
 60     }
 61 
 62     /**
 63      * 通过拼接的方式构造请求内容,实现参数传输以及文件传输
 64      *
 65      * @param url    Service net address
 66      * @param params text content
 67      * @param files  pictures
 68      * @return String result of Service response
 69      * @throws IOException
 70      */
 71     public static String post(String url, Map<String, String> params, File files)
 72             throws IOException {
 73         String BOUNDARY = java.util.UUID.randomUUID().toString();
 74         String PREFIX = "--", LINEND = "\r\n";
 75         String MULTIPART_FROM_DATA = "multipart/form-data";
 76         String CHARSET = "UTF-8";
 77 
 78 
 79         Log.i("UploadService", url);
 80         URL uri = new URL(url);
 81         HttpURLConnection conn = (HttpURLConnection) uri.openConnection();
 82         conn.setReadTimeout(10 * 1000); // 缓存的最长时间
 83         conn.setDoInput(true);// 允许输入
 84         conn.setDoOutput(true);// 允许输出
 85         conn.setUseCaches(false); // 不允许使用缓存
 86         conn.setRequestMethod("POST");
 87         conn.setRequestProperty("connection", "keep-alive");
 88         conn.setRequestProperty("Charsert", "UTF-8");
 89         conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY);
 90 
 91         // 首先组拼文本类型的参数
 92         StringBuilder sb = new StringBuilder();
 93         for (Map.Entry<String, String> entry : params.entrySet()) {
 94             sb.append(PREFIX);
 95             sb.append(BOUNDARY);
 96             sb.append(LINEND);
 97             sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINEND);
 98             sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND);
 99             sb.append("Content-Transfer-Encoding: 8bit" + LINEND);
100             sb.append(LINEND);
101             sb.append(entry.getValue());
102             sb.append(LINEND);
103         }
104 
105 
106         DataOutputStream outStream = new DataOutputStream(conn.getOutputStream());
107         outStream.write(sb.toString().getBytes());
108         // 发送文件数据
109         if (files != null) {
110             StringBuilder sb1 = new StringBuilder();
111             sb1.append(PREFIX);
112             sb1.append(BOUNDARY);
113             sb1.append(LINEND);
114             sb1.append("Content-Disposition: form-data; name=\"uploadfile\"; filename=\""
115                     + files.getName() + "\"" + LINEND);
116             sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND);
117             sb1.append(LINEND);
118             outStream.write(sb1.toString().getBytes());
119 
120 
121             InputStream is = new FileInputStream(files);
122             byte[] buffer = new byte[1024];
123             int len = 0;
124             while ((len = is.read(buffer)) != -1) {
125                 outStream.write(buffer, 0, len);
126             }
127 
128             is.close();
129             outStream.write(LINEND.getBytes());
130         }
131 
132 
133         // 请求结束标志
134         byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes();
135         outStream.write(end_data);
136         outStream.flush();
137         // 得到响应码
138         int res = conn.getResponseCode();
139         Log.i("UploadService", String.valueOf(res));
140         InputStream in = conn.getInputStream();
141         StringBuilder sb2 = new StringBuilder();
142         if (res == 200) {
143             int ch;
144             while ((ch = in.read()) != -1) {
145                 sb2.append((char) ch);
146             }
147         }
148         outStream.close();
149         conn.disconnect();
150         return sb2.toString();
151     }
152 }

 

在build.gradle新增

apply plugin: 'jacoco'

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

 

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

1 @Override
2 public boolean onKeyDown(int keyCode, KeyEvent event) {
3     if (keyCode == KeyEvent.KEYCODE_BACK) {
4         ....
5 
6         JacocoUtils.generateEcFile(true);
7     }
8 
9 }

 

2. git apply patch

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

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

然后git log查看commit

 

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

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

 

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

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

执行成功后的输出:

 

 

3. 服务器生成jacoco覆盖率报告

在服务器我也拉了一个Android工程,专门用于生成报告

主要在build.gradle新增

 1 def coverageSourceDirs = [
 2         '../app/src/main/java'
 3 ]
 4 
 5 task jacocoTestReport(type: JacocoReport) {
 6     group = "Reporting"
 7     description = "Generate Jacoco coverage reports after running tests."
 8     reports {
 9         xml.enabled = true
10         html.enabled = true
11     }
12     classDirectories = fileTree(
13             dir: './build/intermediates/classes/debug',
14             excludes: ['**/R*.class',
15                        '**/*$InjectAdapter.class',
16                        '**/*$ModuleAdapter.class',
17                        '**/*$ViewInjector*.class'
18             ])
19     sourceDirectories = files(coverageSourceDirs)
20     executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
21 
22     doFirst {
23         new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
24             if (file.name.contains('$$')) {
25                 file.renameTo(file.path.replace('$$', '$'))
26             }
27         }
28     }
29 }

 

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

gradle createDebugCoverageReport
gradle jacocoTestReport

 

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