基于JaCoCo的Android测试覆盖率统计(二)
本文章是我上一篇文章的升级版本,详见地址:https://www.cnblogs.com/xiaoluosun/p/7234606.html
为什么要做这个?
- 辛辛苦苦写了几百条测试用例,想知道这些用例的覆盖率能达到多少?
- 勤勤恳恳验证好几天,也没啥bug了,可不可以上线?有没有漏测的功能点?
- 多人协同下测试,想了解团队每个人的测试进度、已覆盖功能点、验证过的设备机型和手机系统等等。
数据采集和上报
既然要做覆盖率分析,数据的采集非常重要,除了JaCoCo生成的.ec文件之外,还需要拿到额外一些信息,如被测设备系统版本、系统机型、App的版本、用户唯一标识(UID)、被测环境等等。
什么时候触发数据的上报呢?这个机制很重要,如果设计的不合理,覆盖率数据可能会有问题。
最早使用的上报策略是:加在监听设备按键的位置,如果点击设备back键或者home键把App置于后台,则上报覆盖率数据。
这种设计肯定是会有问题的,因为有些时候手机设备用完就扔那了,根本没有置于后台,第二天可能才会继续使用,这时候上报的数据就变成了第二天的。还可能用完之后杀死了App,根据就不会上报,覆盖率数据造成丢失;
所以优化后的上报策略是:定时上报,每一分钟上报一次,只要App进程活着就会上报。
那怎么解决用完就杀死App的问题呢?解决办法是App重新启动后查找ec文件目录,如果有上次的记录就上报,这样就不会丢覆盖率数据了。
生成覆盖率文件
1 /** 2 * Created by sun on 17/7/4. 3 */ 4 5 public class JacocoUtils { 6 static String TAG = "JacocoUtils"; 7 8 //ec文件的路径 9 private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec"; 10 11 /** 12 * 生成ec文件 13 * 14 * @param isNew 是否重新创建ec文件 15 */ 16 public static void generateEcFile(boolean isNew) { 17 // String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec"; 18 Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH); 19 OutputStream out = null; 20 File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH); 21 try { 22 if (isNew && mCoverageFilePath.exists()) { 23 Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件"); 24 mCoverageFilePath.delete(); 25 } 26 if (!mCoverageFilePath.exists()) { 27 mCoverageFilePath.createNewFile(); 28 } 29 out = new FileOutputStream(mCoverageFilePath.getPath(), true); 30 31 Object agent = Class.forName("org.jacoco.agent.rt.RT") 32 .getMethod("getAgent") 33 .invoke(null); 34 35 out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class) 36 .invoke(agent, false)); 37 38 // ec文件自动上报到服务器 39 UploadService uploadService = new UploadService(mCoverageFilePath); 40 uploadService.start(); 41 } catch (Exception e) { 42 Log.e(TAG, "generateEcFile: " + e.getMessage()); 43 } finally { 44 if (out == null) 45 return; 46 try { 47 out.close(); 48 } catch (IOException e) { 49 e.printStackTrace(); 50 } 51 } 52 } 53 }
采集到想要的数据上传服务器
1 /** 2 * Created by sun on 17/7/4. 3 */ 4 5 public class UploadService extends Thread{ 6 7 private File file; 8 public UploadService(File file) { 9 this.file = file; 10 } 11 12 public void run() { 13 Log.i("UploadService", "initCoverageInfo"); 14 // 当前时间 15 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 16 Calendar cal = Calendar.getInstance(); 17 String create_time = format.format(cal.getTime()).substring(0,19); 18 19 // 系统版本 20 String os_version = DeviceUtils.getSystemVersion(); 21 22 // 系统机型 23 String device_name = DeviceUtils.getDeviceType(); 24 25 // 应用版本 26 String app_version = DeviceUtils.getAppVersionName(LuojiLabApplication.getInstance()); 27 28 // 应用版本 29 String uid = String.valueOf(AccountUtils.getInstance().getUserId()); 30 31 // 环境 32 String context = String.valueOf(BuildConfig.SERVER_ENVIRONMENT); 33 34 Map<String, String> params = new HashMap<String, String>(); 35 params.put("os_version", os_version); 36 params.put("device_name", device_name); 37 params.put("app_version", app_version); 38 params.put("uid", uid); 39 params.put("context", context); 40 params.put("create_time", create_time); 41 42 try { 43 post("https://xxx.com/coverage/uploadec", params, file); 44 } catch (IOException e) { 45 e.printStackTrace(); 46 } 47 48 } 49 50 /** 51 * 通过拼接的方式构造请求内容,实现参数传输以及文件传输 52 * 53 * @param url Service net address 54 * @param params text content 55 * @param files pictures 56 * @return String result of Service response 57 * @throws IOException 58 */ 59 public static String post(String url, Map<String, String> params, File files) 60 throws IOException { 61 String BOUNDARY = java.util.UUID.randomUUID().toString(); 62 String PREFIX = "--", LINEND = " "; 63 String MULTIPART_FROM_DATA = "multipart/form-data"; 64 String CHARSET = "UTF-8"; 65 66 67 Log.i("UploadService", url); 68 URL uri = new URL(url); 69 HttpURLConnection conn = (HttpURLConnection) uri.openConnection(); 70 conn.setReadTimeout(10 * 1000); // 缓存的最长时间 71 conn.setDoInput(true);// 允许输入 72 conn.setDoOutput(true);// 允许输出 73 conn.setUseCaches(false); // 不允许使用缓存 74 conn.setRequestMethod("POST"); 75 conn.setRequestProperty("connection", "keep-alive"); 76 conn.setRequestProperty("Charsert", "UTF-8"); 77 conn.setRequestProperty("Content-Type", MULTIPART_FROM_DATA + ";boundary=" + BOUNDARY); 78 79 // 首先组拼文本类型的参数 80 StringBuilder sb = new StringBuilder(); 81 for (Map.Entry<String, String> entry : params.entrySet()) { 82 sb.append(PREFIX); 83 sb.append(BOUNDARY); 84 sb.append(LINEND); 85 sb.append("Content-Disposition: form-data; name="" + entry.getKey() + """ + LINEND); 86 sb.append("Content-Type: text/plain; charset=" + CHARSET + LINEND); 87 sb.append("Content-Transfer-Encoding: 8bit" + LINEND); 88 sb.append(LINEND); 89 sb.append(entry.getValue()); 90 sb.append(LINEND); 91 } 92 93 DataOutputStream outStream = new DataOutputStream(conn.getOutputStream()); 94 outStream.write(sb.toString().getBytes()); 95 // 发送文件数据 96 if (files != null) { 97 StringBuilder sb1 = new StringBuilder(); 98 sb1.append(PREFIX); 99 sb1.append(BOUNDARY); 100 sb1.append(LINEND); 101 sb1.append("Content-Disposition: form-data; name="uploadfile"; filename="" 102 + files.getName() + """ + LINEND); 103 sb1.append("Content-Type: application/octet-stream; charset=" + CHARSET + LINEND); 104 sb1.append(LINEND); 105 outStream.write(sb1.toString().getBytes()); 106 107 InputStream is = new FileInputStream(files); 108 byte[] buffer = new byte[1024]; 109 int len = 0; 110 while ((len = is.read(buffer)) != -1) { 111 outStream.write(buffer, 0, len); 112 } 113 114 is.close(); 115 outStream.write(LINEND.getBytes()); 116 } 117 118 119 // 请求结束标志 120 byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes(); 121 outStream.write(end_data); 122 outStream.flush(); 123 // 得到响应码 124 int res = conn.getResponseCode(); 125 Log.i("UploadService", String.valueOf(res)); 126 InputStream in = conn.getInputStream(); 127 StringBuilder sb2 = new StringBuilder(); 128 if (res == 200) { 129 int ch; 130 while ((ch = in.read()) != -1) { 131 sb2.append((char) ch); 132 } 133 } 134 outStream.close(); 135 conn.disconnect(); 136 return sb2.toString(); 137 } 138 }