基於JaCoCo的Android測試覆蓋率統計(二)

本文章是我上一篇文章的升級版本,詳見地址:https://www.cnblogs.com/xiaoluosun/p/7234606.htmlhtml

爲何要作這個?

  1. 辛辛苦苦寫了幾百條測試用例,想知道這些用例的覆蓋率能達到多少?
  2. 勤勤懇懇驗證好幾天,也沒啥bug了,可不能夠上線?有沒有漏測的功能點?
  3. 多人協同下測試,想了解團隊每一個人的測試進度、已覆蓋功能點、驗證過的設備機型和手機系統等等。

數據採集和上報

既然要作覆蓋率分析,數據的採集很是重要,除了JaCoCo生成的.ec文件以外,還須要拿到額外一些信息,如被測設備系統版本、系統機型、App的版本、用戶惟一標識(UID)、被測環境等等。java

何時觸發數據的上報呢?這個機制很重要,若是設計的不合理,覆蓋率數據可能會有問題。正則表達式

最先使用的上報策略是:加在監聽設備按鍵的位置,若是點擊設備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 }
View Code

採集到想要的數據上傳服務器

 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 = "\r\n";  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 }
View Code

上報數據的定時器

1 /**
2  * 定時器,每分鐘調用一次生成覆蓋率方法 3  * 4  */
5 public boolean timer() { 6     JacocoUtils.generateEcFile(true); 7 }
View Code

啓用JaCoCo

安裝plugin
1 apply plugin: 'jacoco'
2 
3 jacoco { 4     toolVersion = '0.7.9'
5 }
View Code  
啓用覆蓋率開關

此處是在debug時啓用覆蓋率的收集服務器

Android 9.0以上版本由於限制私有API的集成,因此若是打開了開關,9.0以上系統使用App時會有系統級toast提示「Detected problems with API compatibility」,但不影響功能。app

1 buildTypes { 2  debug { 3         testCoverageEnabled = true
4  } 5 }
View Code

分析源碼和二進制,生成覆蓋率報告

執行命令生成dom

1 ./gradlew jacocoTestReport
View Code

這塊作的時候遇到三個問題。
第一個問題是App已經拆成組件了,每一個主要模塊都是一個可獨立編譯的業務組件。若是按照以前的方法只能統計到主工程的覆蓋率,業務組件的覆蓋率統計不到。
解決辦法是是先拿到全部業務組件的名稱和路徑(咱們在settings.gradle裏有定義),而後循環添加成一個list,files方法支持list當作二進制目錄傳入。編程語言

第二個問題是部分業務組件是用Kotlin開發的,因此要同時兼容Java和Kotlin兩種編程語言。
解決辦法跟問題一的同樣,files同時支持Kotlin的二進制目錄傳入。ide

第三個問題是覆蓋率數據是碎片式的,天天會有上萬個覆蓋率文件生成,以前只作過單個文件的覆蓋率計算,如何批量計算覆蓋率文件?
解決辦法是使用fileTree方法的includes,用正則表達式*號,批量計算特定目錄下符合規則的全部.ec文件。

1 executionData = fileTree(dir: "$buildDir", includes: [ 2     "outputs/code-coverage/connected/*coverage.ec"
3 ])
View Code

完整代碼

 1 task jacocoTestReport(type: JacocoReport) {  2     def lineList = new File(project.rootDir.toString() + '/settings.gradle').readLines()  3     def coverageCompName = []  4     for (i in lineList) {  5         if (!i.isEmpty() && i.contains('include')) {  6             coverageCompName.add(project.rootDir.toString() + '/' + i.split(':')[1].replace("'", '') + '/')  7  }  8  }  9 
10     def coverageSourceCompName = [] 11     for (i in lineList) { 12         if (!i.isEmpty() && i.contains('include')) { 13             coverageSourceCompName.add('../' + i.split(':')[1].replace("'", '') + '/') 14  } 15  } 16 
17  reports { 18         xml.enabled = true
19         html.enabled = true
20  } 21     def fileFilter = ['**/R*.class', 22                       '**/*$InjectAdapter.class', 23                       '**/*$ModuleAdapter.class', 24                       '**/*$ViewInjector*.class', 25                       '**/*Binding*.class', 26                       '**/*BR*.class'
27  ] 28 
29     def coverageSourceDirs = [] 30     for (i in coverageSourceCompName) { 31         def sourceDir = i + 'src/main/java'
32  coverageSourceDirs.add(sourceDir) 33  } 34 
35     def coverageClassDirs = [] 36     for (i in coverageCompName) { 37         def classDir = fileTree(dir: i + 'build/intermediates/classes/release', excludes: fileFilter) 38  coverageClassDirs.add(classDir) 39  } 40 
41     def coverageKotlinClassDirs = [] 42     for (i in coverageCompName) { 43         def classKotlinDir = fileTree(dir: i + 'build/tmp/kotlin-classes/release', excludes: fileFilter) 44  coverageKotlinClassDirs.add(classKotlinDir) 45  } 46 
47     classDirectories = files(coverageClassDirs, coverageKotlinClassDirs) 48     sourceDirectories = files(coverageSourceDirs) 49 // executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
50     executionData = fileTree(dir: "$buildDir", includes: [ 51             "outputs/code-coverage/connected/*coverage.ec"
52  ]) 53 
54  doFirst { 55         new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
56             if (file.name.contains('$$')) { 57                 file.renameTo(file.path.replace('$$', '$')) 58  } 59  } 60  } 61 }
View Code

數據分析和處理

待補充。。。。

應用環境的覆蓋率分析

 

 

 

設備系統的覆蓋率分析

 

 

 

用戶UID的覆蓋率分析

 

 

 

應用版本的覆蓋率分析

 

 

原文出處:https://www.cnblogs.com/xiaoluosun/p/11304178.html

相關文章
相關標籤/搜索