當前位置:
首頁 > 最新 > Android增量代碼測試覆蓋率工具

Android增量代碼測試覆蓋率工具

前言

美團點評業務快速發展,新項目新業務不斷出現,在項目開發和測試人員不足、開發同學粗心的情況下,難免會出現少測漏測的情況,如何保證新增代碼有足夠的測試覆蓋率是我們需要思考的問題。

Bad-Case

先看一個bug:

以上代碼可能在onDestory時反註冊一個沒有註冊的receiver而發生崩潰。如果開發同學經驗不足、自測不夠充分或者代碼審查不夠仔細,這個bug很容易被帶到線上。

正常情況下,可以通過寫單測來保證新增代碼的覆蓋率,在Android中可以參考《Android單元測試研究與實踐》。但在實際開發中,由於單測部署成本高、項目排期比較緊張、需求變化頻繁、團隊成員能力不足等多種原因,單測在互聯網行業普及程度並不理想。

所以我們實現了這樣一個工具,不需要寫單測的情況下,在代碼提交之前自動檢測新增代碼的手工測試覆蓋率,避免新開發的功能沒有經過自測就直接進入代碼審查環節。

整個工具主要包含下面三個方面的內容:

如何獲取新增代碼。

如何只生成新增代碼的覆蓋率報告。

如何讓整個流程自動化。

獲取新增代碼定義新增代碼

美團點評一直使用Git做代碼版本控制,開發完之後提交pull request到目標分支,審查通過後即可合并。所以對於單次提交,可將新增的代碼定義為:

本地工作目錄中還沒提交到暫存區的代碼。

已經提交到暫存區的代碼。

上次merge以後到還沒有merge的commit中的代碼。

如下圖所示:

得到新增代碼的定義以後,如何得到這些文件中真正新增的代碼:

把當前檢測變化的Java文件放到一個臨時目錄A中。

分別查看第一步找到的文件在最近一個merge的commit中的文件,並放到臨時目錄B中。

為了充分測試修改的代碼,這裡把方法作為最小測試單元(新增和修改的方法),即使是修改了方法中的某一行代碼也認為這個方法發生了變化。如何準確定位到哪些方法發生了變化?我們通過抽象語法樹來實現。

抽象語法樹

所謂抽象語法樹,就是源代碼的抽象語法結構的樹狀表現形式,樹上的每一個節點代表源代碼中的一種結構。

下面通過Android Studio的JDT-View插件來表示一個簡單的抽象語法樹結構,左邊是源碼,右邊是解析完以後的抽象語法結構:

後續語法樹分析的實現通過Eclipse的JDT來完成。用JDT主要解決兩個問題:

定位哪些方法發生了變化。

把JDT分析出的結果轉化為合適的數據結構,方便後面做增量注入。

第一個問題比較容易解決,分別生成兩組Java文件(上一部分結尾得到的兩組文件A、B)的語法樹,並對方法(去掉注釋和空行)進行MD5,MD5不同的方法,便認為該方法在這次提交中發生了變化。

對於第二個問題,主要的難點在於通過JDT得到的方法定義和通過ASM(後面位元組碼注入通過ASM來實現)得到的方法定義不同,這二者最大的區別是JDT無法直接得到內部類、匿名內部類、Lambda表達式的ClassName,所以需要在語法樹分析時把方法對應的ClassName轉化成位元組碼對應的ClassName。位元組碼生成內部類和RetroLambdaClassName的規則如下:

匿名內部類:...$Index。

普通內部類、靜態內部類:...$InnerClassName。

RetroLambda表達式:...$$Lambda$Index。

具體如何處理呢?JDT在分析Java文件時有幾個關鍵的函數:

visit(MethodDeclaration method):訪問普通方法的定義。

visit(AnonymousDeclaration method):訪問匿名內部類的定義。

endVisit(AnonymousDeclaration method):結束匿名內部類的定義。

visit(TypeDeclaration node):訪問普通類定義。

endVisit(TypeDeclaration node):結束普通類的定義。

visit(LambdaExpress node):訪問Lambda表達式的定義。

同時在解析源文件時會按照源碼定義順序來訪問各個節點。對於以上情況,只需要按照入棧和出棧的順序來管理ClassName,就能和後面位元組碼得到的方法所匹配。

通過以上步驟,把每個方法的信息封裝到MethodInfo中(後面注入和生成覆蓋率報告時會用到該數據):

public String className;//hash package public String md5; public String methodName; public List paramList = new ArrayList(); public String methodBody; public boolean isLambda; //標識是否是Lambda表達式方法 public int lambdaNumInClass; //同一個Class中此lambda表達式是第幾個. 從1開始. public int totalLambdaInClass; //同一個Class中lambda表達式的總數 public String lambdaParent; //lambda表達式的父節點 public boolean isLambdaInAnonymous; //標識lambda表達式是否位於內部類中 public boolean isAnonymousClass; //標識是否是內部類方法新增代碼的覆蓋率報告

生成代碼的覆蓋率報告,首先想到的就是JaCoCo,下面分別介紹一下JaCoCo的原理和我們所做的改造。

JaCoCo概述

JaCoCo包含了多種維度的覆蓋率計數器:指令級計數器(C0 coverage)、分支級計數器(C1 coverage)、圈複雜度、行覆蓋、方法覆蓋、類覆蓋。其覆蓋率報告的示例如下:

綠色:表示行覆蓋充分。

紅色:表示未覆蓋的行。

黃色棱形:表示分支覆蓋不全。

綠色棱形:表示分支覆蓋完全。

注入原理

JaCoCo主要通過代碼注入的方式來實現上面覆蓋率的功能。JaCoCo支持的注入方式如下圖(圖片出自這裡)所示:

包含了幾種不同的收集覆蓋率信息的方法,每個方法的實現都不太一樣,這裡主要關心位元組碼注入這種方式(Byte Code)。Byte Code包含Offline和On-The-Fly兩種注入方式:

Offline:在生成最終的目標文件之前,對Class文件進行插樁,生成最終的目標文件,執行目標文件以後得到覆蓋執行結果,最終生成覆蓋率報告。

On-The-Fly:JVM通過-javaagent指定特定的Jar來啟動Instrumentation代理程序,代理程序在ClassLoader裝載一個class前先判斷是否需要對class進行注入,對於需要注入的class進行注入。覆蓋率結果可以在JVM執行代碼的過程中完成。

可以看到,On-The-Fly因為要修改JVM參數,所以對環境的要求比較高,為了屏蔽工具對虛擬機環境的依賴,我們的代碼注入主要選擇Offline這種方式。

Offline的工作流程:

在生成最終目標文件之前對位元組碼進行插樁。

運行測試代碼,得到運行時數據。

根據運行時數據、生成的class文件、源碼生成覆蓋率報告。

通過一張圖來形象地表示一下:

如何實現代碼注入呢?舉個例子說明一下:

JaCoCo通過ASM在位元組碼中插入Probe指針(探測指針),每個探測指針都是一個BOOL變數(true表示執行、false表示沒有執行),程序運行時通過改變指針的結果來檢測代碼的執行情況(不會改變原代碼的行為)。探測指針完整插入策略請參考Probe Insertion Strategy。

增量注入

介紹完JaCoCo注入原理以後,我們來看看如何做到增量注入:

JaCoCo默認的注入方式為全量注入。通過閱讀源碼,發現注入的邏輯主要在ClassProbesAdapter中。ASM在遍歷位元組碼時,每次訪問一個方法定義,都會回調這個類的visitMethod方法

,在visitMethod方法中再調用ClassProbeVisitor的visitMethod方法,並最終調用MethodInstrumenter完成注入。部分代碼片段如下:

@Override public final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { final MethodProbesVisitor methodProbes; final MethodProbesVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (mv == null) { methodProbes = EMPTY_METHOD_PROBES_VISITOR; } else { methodProbes = mv; } return new MethodSanitizer(null, access, name, desc, signature, exceptions) { @Override public void visitEnd() { super.visitEnd(); LabelFlowAnalyzer.markLabels(this); final MethodProbesAdapter probesAdapter = new MethodProbesAdapter( methodProbes, ClassProbesAdapter.this); if (trackFrames) { final AnalyzerAdapter analyzer = new AnalyzerAdapter( ClassProbesAdapter.this.name, access, name, desc, probesAdapter); probesAdapter.setAnalyzer(analyzer); this.accept(analyzer); } else { this.accept(probesAdapter); } } }; }

看到這裡基本上已經知道如何去修改JaCoCo的源碼了。繼承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只對變化了方法進行注入:

@Override public final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { if (Utils.shoudHackMethod(name,desc,signature,changedMethods,cv.getClassName())) { ... } else { return cv.getCv().visitMethod(access, name, desc, signature, exceptions); } }生成增量代碼的覆蓋率報告

和增量注入的原理類似,通過閱讀源碼,分別需要修改Analyzer(只對變化的類做處理):

@Override public void analyzeClass(final ClassReader reader) { if (Utils.shoudHackMethod(reader.getClassName(),changedMethods)) { ... } }

和ReportClassProbesAdapter(只對變化的方法做處理):

@Override public final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { if (Utils.shoudHackMethod(name, desc, signature, changedMethods, this.className)) { ... } else { return null; } }

這樣就能生成新增代碼的覆蓋率報告。如下圖所示本次commit只修改了FoodPoiDetailActivity的onCreate和initCustomTitle這兩個方法,那麼覆蓋率只涉及這些修改了的方法:

JDT vs ASM

在上面增量注入和生成增量代碼覆蓋率報告時都會去判斷當前方法是否應該被處理。這裡分別對比JDT和ASM解析結果中的className、methodName、paramList來判斷當前方法是否需要被注入,部分代碼片段:

public static boolean shoudHackMethod(String methodName, String desc, String signature, HashSet changedMethods, String className) { Map changedLambdaMethods = getChangedLambdaMethods(changedMethods); List changedLambdaMethodNames = changedLambdaMethods.get(className.replace("/", ".")); updateLambdaNum(methodName, className); int indexMethods = 0; outer: for (; indexMethods

0) { //兩者方法名相等 if (methodInfo.methodName.equals(methodName)) { changedLambdaMethodNames.remove(methodInfo.methodName) return true; } else if (!changedLambdaMethodNames.contains(methodName)) { //兩者方法名不等,且不包含在改變的lambda方法中,通過載入順序來判斷 int lastIndex = methodInfo.methodName.lastIndexOf( $ ); if (lastIndex 0 && leftBrace > rightBrace) { //只取形參 declaration = declaration.substring(rightBrace + 1, leftBrace); } //勿用\[\]作為分隔符, 否則數組形參不可區分 String paraStr = declaration.replaceAll("[(){}]", ""); if (paraStr.length() > 0) { String[] parasArray = getAsmMethodParams(paraStr.split(","), className, methodInfo.paramList); List paramListAst = getAstMethodParams(methodInfo.paramList); if (parasArray.length == paramListAst.size()) { for (int i = 0; i

. 作為分隔符 String[] methodInfoParamArray = paramListAst.get(i).split("|\."); for (String param : methodInfoParamArray) { if (!parasArray[i].contains(param) || (parasArray[i].contains(param) && parasArray[i].contains("[]") && !param.endsWith("[]"))) { //同類名、同方法名、同參數長度, 參數類型不一致(或者 比較相等, 但class中是數組, 而源碼中不是數組) 跳轉到 outer循環開始處 continue outer; } } } } else { continue; } } if (methodInfo.isLambda && changedLambdaMethodNames != null) { changedLambdaMethodNames.remove(methodInfo.methodName) } return true; } } } } return false; }流程的自動化自動注入

整個工具通過Gradle插件的形式加入到項目中,只需要簡單配置即可使用,在生成DEX之前完成增量代碼的注入,同時為了不影響線上版本,該插件只在Debug模式下生效。

自動獲取運行時數據

剛才講JaCoCo原理的時候提到,需要運行時數據才能生成覆蓋率報告。代碼中通過反射執行下面的函數來獲取運行時數據,並保存到當前執行代碼的設備中:

由於生成報告時需要用到運行時數據,為了生成的覆蓋率報告更準確、開發同學用起來更方便,分別在如下時機把運行時數據保存到當前設備中:

每個頁面執行onDestory時。

程序發生崩潰時。

收到特定廣播(一個自定義的廣播,在執行生成覆蓋率報告的task前發送)時。

並在生成覆蓋率報告之前把設備中的運行時數據同步到本地開發環境中。

上面可以看到,因為獲取時機比較多,可能會得到多份運行時數據,對於這些數據,可以通過JaCoCo的mergeTask把ClassId相同的運行時數據進行merge。如下圖所示,JaCoCo會對ClassId相同的運行時數據進行merge,並對相同位置的probe指針取或:

自動部署Pre-Push腳本

為了開發者在提交代碼之前能夠自動生成覆蓋率報告,我們在插件apply階段動態下發一個Pre-Push腳本到本地項目的.git目錄。在push之前生成覆蓋率報告,同時對於覆蓋率小於一定值(默認95%,可自定義)的提交提示並報警:

整體流程圖

整個工具通過Gradle插件的形式部署到項目中,在項目編譯階段完成新增代碼的查找和注入,在最終push代碼之前獲取當前設備的運行時數據,然後生成覆蓋率報告,並把覆蓋率低於一定值(默認是95%)的提交abort掉。

最後通過一張完整的圖來看下這個工具的工作流程:

總結

上述是我們在保障開發質量方面做的一些探索和積累。通過保障開發階段增量代碼的自測覆蓋率,讓開發者充分檢驗開發效果,提前發現邏輯缺陷,將風險前置。保障開發質量的道路任重而道遠, 我們可以通過良好的測試覆蓋率、持續完善單測、改善代碼框架、規範開發流程等等多種維度相輔相成、共同推進。

作者介紹

本文三位作者均來自美團點評的到店餐飲技術部信息與交易技術中心。

武智,Android高級開發工程師,2013年7月校招加入美團點評,目前負責維護大眾點評App的美食頻道。

瑩瑩,2015年校招加入美團點評,主要參與大眾點評美食頻道的日常開發工作,專註於通過工具自動化地提高開發效率和質量。

周佳,2016年校招加入美團點評,主要參與大眾點評美食頻道的日常開發工作。

到店餐飲技術部交易與信息技術中心,負責美團點評美食用戶端業務,服務於數以億計用戶,通過更好的榜單、真實的評價和完善的信息為用戶提供更好的決策支持,致力於提升用戶體驗;同時承載所有餐飲商戶端線上流量,為餐飲商戶提供多種營銷工具,提升餐飲商戶營銷效率,最終達到讓國人「Eat Better、Live Better」的美好願景!我們的團隊包含且不限於Android、iOS、FE、Java、PHP等技術方向,已完備覆蓋前後端技術棧。只要你來,就能點亮全棧開發技能樹。誠摯歡迎投遞簡歷至chenhongbing#meituan.com。

【思考題】

本文為大家介紹的工具基本上可以解決新增代碼沒有覆蓋導致的問題。但開發過程中還會有一些因為數據、狀態錯誤導致的問題,對於這類問題,通過什麼工具可以及時的發現並解決?日常測試過程中用到測試數據是否被有效的利?和積累,是否能利用大數據相關的技術完善新時代的測試體系?

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 推酷 的精彩文章:

復盤零售業10年,賣貨的和賣貨的都在拼什麼?
Industroyer去年襲擊烏克蘭電網?這可能是震網之後最危險的工控惡意程序
資深產品經理是如何做需求管理的:需求的優先順序判定原則
流動的藏私:論匿名社交
手把手教你如何通過流氓WiFi熱點實施網路釣魚

TAG:推酷 |

您可能感興趣

jacoco與jenkins集成實現代碼覆蓋率分析
用pytest測試python:夾具和覆蓋率
手機系統更新現狀:iOS覆蓋率最強、安卓更新最快、windows最凄涼
OpenSignal:全球已經有五個國家4G覆蓋率超過90%
增量代碼覆蓋率工具
LG Uplus 5G覆蓋率低被詬病 聯手三星拉開反擊戰序幕
Win10 1709覆蓋率佔Win10九成!
全國ETC通道覆蓋率將達到90% ETC助手助車主秒辦ETC
Win10創意者更新秋季版覆蓋率已達85%
2017全球4G LTE覆蓋率日本領先
單元測試與覆蓋率
華為就是牛,5G訂單又漲了!全球5G覆蓋率達2/3,實力強硬
Win10秋季創意者更新覆蓋率達85%:中國區拖後腿
2017全球4G LTE覆蓋率日本領先 新加坡荷蘭網速最快
工行去年凈利增3%,撥備覆蓋率反轉增至154.07%
世界上WIFI信號最強的國家,覆蓋率達100%,隨時隨地「網上衝浪」
大連農商行2017不良率升至4.95% 撥備覆蓋率不達監管要求
比特幣ATM覆蓋率排行:安圭拉居首,美國第九
銀監會下調撥備紅線 撥備覆蓋率最低120%;保監會出台新規 資管計劃持股不得超5%
空氣質量超好的景區,森林覆蓋率高達97%,負氧離子含量非常高