轉(zhuǎn)轉(zhuǎn)App代碼覆蓋率方案
作者|張志陽(yáng)
代碼覆蓋率是業(yè)內(nèi)常用的統(tǒng)計(jì)代碼被執(zhí)行程度的手段。覆蓋率數(shù)據(jù)結(jié)果是對(duì)測(cè)試工作的一種保證,可以贏得信任、增加上線信心。今天就讓我們來(lái)聊聊?轉(zhuǎn)轉(zhuǎn)App代碼覆蓋率方案的實(shí)現(xiàn)及應(yīng)用,希望可以給大家?guī)?lái)一些思路和參考。
目的
在轉(zhuǎn)轉(zhuǎn)客戶端團(tuán)隊(duì),搭建代碼覆蓋率方案的主要目的:
通過(guò)代碼被執(zhí)行的比例程度,表現(xiàn)測(cè)試工作的覆蓋程度
通過(guò)未覆蓋內(nèi)容的分析&補(bǔ)充測(cè)試,保證測(cè)試覆蓋程度
通過(guò)分析無(wú)法覆蓋的內(nèi)容,判斷代碼設(shè)計(jì)的合理性
這里需要補(bǔ)充強(qiáng)調(diào)一點(diǎn):覆蓋率只是一種度量測(cè)試完整性的手段, 是一種測(cè)試有效性的度量,代碼覆蓋率100%也并不代表不會(huì)有其他的問(wèn)題。
方案選擇
當(dāng)我們想實(shí)現(xiàn)一套專(zhuān)項(xiàng)測(cè)試方案的時(shí)候,首先都會(huì)想到“在業(yè)內(nèi)有沒(méi)有公開(kāi)的、流行的、可用的方案,可供參考或者 直接可以”拿來(lái)主義”。
首先,我們可以在搜索引擎、論壇上去進(jìn)行搜索 “App代碼覆蓋率”, 結(jié)果發(fā)現(xiàn)每種語(yǔ)言都有自己的覆蓋率數(shù)據(jù)收集方式。
比如現(xiàn)在服務(wù)端代碼覆蓋率使用的Jacoco,是一套非常成熟的Java 方案。
python 、C++、OC 也都有自己體系的覆蓋率收集方案,但并沒(méi)有Jacoco 那么方便集成。更沒(méi)有已經(jīng)將多種語(yǔ)言的覆蓋率統(tǒng)計(jì)集成在一起的方案。
在調(diào)研&對(duì)比幾種方案后,都會(huì)發(fā)現(xiàn)了一些缺點(diǎn)、功能缺失?和不方便使用的地方,所以開(kāi)始考慮,能否從0實(shí)現(xiàn)一套完全適用自己團(tuán)隊(duì)的代碼覆蓋率方案。
方案構(gòu)思
在常見(jiàn)的代碼結(jié)構(gòu)中,“行”是代碼工程的最小單元,所以想要實(shí)現(xiàn)代碼覆蓋率方案來(lái)統(tǒng)計(jì)代碼的被覆蓋程度,首先我們應(yīng)該都會(huì)想到,我們需要統(tǒng)計(jì)所有“代碼行”的覆蓋情況,“行”應(yīng)該是最小的統(tǒng)計(jì)單元。
在大家日常的測(cè)試工作中,應(yīng)該經(jīng)常會(huì)做?埋點(diǎn)測(cè)試,簡(jiǎn)單的說(shuō),就是通過(guò)埋點(diǎn)代碼記錄某個(gè)頁(yè)面入口/操作/事件?的觸發(fā)?或者結(jié)果的返回,當(dāng)頁(yè)面入口/操作/事件?被觸發(fā)后,記錄&上報(bào),埋點(diǎn)系統(tǒng)統(tǒng)計(jì)計(jì)數(shù),進(jìn)行業(yè)務(wù)層次的數(shù)據(jù)分析。
這和我們當(dāng)前要實(shí)現(xiàn)的代碼覆蓋率方案需求有些類(lèi)似,我們想要在代碼行被執(zhí)行后,進(jìn)行記錄&上報(bào),覆蓋率系統(tǒng)進(jìn)行統(tǒng)計(jì),進(jìn)行代碼層次的數(shù)據(jù)分析,與埋點(diǎn)對(duì)比,代碼覆蓋程度更廣一些,但是實(shí)現(xiàn)的方式是比較類(lèi)似的,就是在需要關(guān)注的 代碼/事件后增加?用于“記錄”的代碼塊,這種添加代碼的方式,就是常說(shuō)的“插樁”(* 基本的原則:保證被測(cè)程序原有邏輯的完整性),插入的代碼塊一般稱(chēng)之為“探針”。
所以想要實(shí)現(xiàn)一套代碼覆蓋率方案的基礎(chǔ)就是:
在保證被測(cè)App原有代碼邏輯的完整性的前提下,通過(guò)代碼插樁的方式,在每行源代碼后插入“探針”代碼,記錄對(duì)應(yīng)代碼行已被執(zhí)行
將記錄的代碼覆蓋數(shù)據(jù)上報(bào)給覆蓋率服務(wù)
覆蓋率服務(wù) 將數(shù)據(jù)進(jìn)行保存 & 合并計(jì)算
結(jié)合團(tuán)隊(duì)內(nèi)的實(shí)際方案需求,我們需要先實(shí)現(xiàn)以下部分:
實(shí)現(xiàn)Android、iOS工程的代碼插樁
探針代碼被執(zhí)行時(shí)?記錄對(duì)應(yīng)代碼的執(zhí)行狀態(tài)
為了統(tǒng)一Android 、iOS?覆蓋率數(shù)據(jù)的計(jì)算及使用方式,需要統(tǒng)一記錄時(shí)使用的數(shù)據(jù)結(jié)構(gòu)
代碼執(zhí)行狀態(tài)數(shù)據(jù)上報(bào)、接收、存儲(chǔ)
數(shù)據(jù)合并計(jì)算、存儲(chǔ)
實(shí)現(xiàn)
1、插樁:
(1)需要解決的難點(diǎn)
難點(diǎn)1:?準(zhǔn)確插樁,保證被測(cè)App原有代碼邏輯的完整性
在日??吹降拇a中我們發(fā)現(xiàn),不同的語(yǔ)言,會(huì)對(duì)代碼格式/語(yǔ)法都有不同的要求/規(guī)范,每個(gè)人在寫(xiě)代碼時(shí)的習(xí)慣也有所不同
在不對(duì)代碼內(nèi)容進(jìn)行語(yǔ)義分析就進(jìn)行隨意插樁,可能會(huì)導(dǎo)致編譯失敗/ 影響原有代碼邏輯
語(yǔ)義分析需要對(duì) 代碼格式/語(yǔ)法 有足夠的掌握程度?&?大量的嘗試保證語(yǔ)義分析的足夠全面
難點(diǎn)2:?所有代碼行都進(jìn)行插樁,會(huì)不會(huì)影響客戶端性能
答案是必然的,當(dāng)前客戶端的代碼量,至少已經(jīng)是百萬(wàn)級(jí)別了
如果在每行代碼前后插入探針代碼、在App實(shí)際運(yùn)行過(guò)程中、頻繁的IO運(yùn)算工作,App的 穩(wěn)定性、性能都不敢保證
(2)解題方案
準(zhǔn)確插樁,保證被測(cè)App原有代碼邏輯的完整性
由客戶端RD同學(xué)負(fù)責(zé)插樁方案的具體實(shí)現(xiàn),即高效又可靠
Android: 使用?ASM?對(duì)字節(jié)碼進(jìn)行分析?,再進(jìn)行準(zhǔn)確插樁
iOS: 通過(guò)語(yǔ)義分析,判斷插樁位置,再進(jìn)行準(zhǔn)確插樁
所有代碼行都進(jìn)行插樁,會(huì)不會(huì)影響客戶端性能
初期方案,我們選擇先對(duì)邏輯分支進(jìn)行插樁,大量減少插樁量
優(yōu)點(diǎn):控制了插樁數(shù)量,減少了對(duì)工程性能的影響
弱點(diǎn):只能采集到進(jìn)入分支,不能判斷分支結(jié)束;不能按行統(tǒng)計(jì)計(jì)算, 不能結(jié)合Code Diff 直接判斷?新增/修改代碼的覆蓋情況
(3)插樁前后代碼對(duì)比
Android

iOS

(4)探針代碼解讀
標(biāo)記邏輯分支被執(zhí)行(邏輯分支全局編號(hào))
全局編號(hào)從哪來(lái)?
在遍歷所有類(lèi)文件時(shí),對(duì)識(shí)別出的邏輯分支進(jìn)行編號(hào)
?插入位置怎么獲得
Android:字節(jié)碼中有行號(hào)的描述,通過(guò)ASM解析可以獲取
iOS:代碼遍歷,判斷出邏輯分支時(shí),就已經(jīng)知道當(dāng)前行行號(hào)
(5)插樁流程
獲取方法路徑和方法簽名(用于多個(gè)tag之間邏輯塊執(zhí)行數(shù)據(jù)的比較和合并)
方法路徑:類(lèi)名+方法名可以確定當(dāng)前工程中一個(gè)唯一的方法:
com.wuba.zhuanzhuan.activity.AboutZhuanzhuanActivityonCreate(Landroid/os/Bundle;)
方法簽名:對(duì)方法內(nèi)的所有字節(jié)碼做MD5編碼,如果方法邏輯有修改(增減),那么簽名就會(huì)變化
Tag間對(duì)比:如果相同方法路徑的簽名有變化,則認(rèn)為該方法內(nèi)的所有邏輯分支都需要重新覆蓋測(cè)試
找到邏輯塊
獲取邏輯塊行號(hào)
邏輯塊編號(hào)
將方法路徑、方法簽名、邏輯塊信息存入methodMapping文件

插樁結(jié)束后,將methodMapping文件上傳到服務(wù)器
2、記錄代碼被執(zhí)行:
(1)數(shù)據(jù)存儲(chǔ):為了不影響原始代碼的執(zhí)行效率,探針代碼執(zhí)行記錄數(shù)據(jù)的讀寫(xiě)效率必須得到保證,所以我們的選擇是:
Android:使用字節(jié)數(shù)組(Bitset) 存儲(chǔ) 探針代碼執(zhí)行記錄數(shù)據(jù)
iOS : 在內(nèi)存中申請(qǐng)指定長(zhǎng)度的數(shù)組空間 存儲(chǔ) 探針代碼執(zhí)行記錄
數(shù)組長(zhǎng)度?
邏輯塊編號(hào)完成后,可以知道邏輯塊總數(shù),一個(gè)字節(jié)可以存儲(chǔ)8個(gè)邏輯塊編號(hào),即可計(jì)算出需要的數(shù)組長(zhǎng)度
大約會(huì)占多少空間?
EXP: 20W 邏輯塊 = 2W5 (20W / 8) 字節(jié) = 24.4KB (2W5 / 1024)
(2)探針代碼被執(zhí)行:
探針代碼中,入?yún)⒕褪?邏輯塊 在所有邏輯塊的中編號(hào)
將數(shù)組中編號(hào)對(duì)應(yīng)的位 置為 1 , 代表已覆蓋
3、數(shù)據(jù)上報(bào)、接收、存儲(chǔ):
(1)什么時(shí)候上報(bào)數(shù)據(jù)
如果頻繁上報(bào),可能會(huì)影響App的正常使用,也并沒(méi)有必要。所以考慮在一些 察覺(jué)不到/不太Care的 操作節(jié)點(diǎn)進(jìn)行數(shù)據(jù)上報(bào)。
Android:頁(yè)面的創(chuàng)建、不可見(jiàn)、銷(xiāo)毀 時(shí) 上報(bào)
iOS:App 啟動(dòng)、退后臺(tái)、喚醒 時(shí)上報(bào)
(2)上報(bào)哪些內(nèi)容
project: 與 代碼覆蓋率服務(wù)約定的唯一 覆蓋率項(xiàng)目名,區(qū)分終端,如zhuanzhuanAndroid / zhuanzhuanIos
version: 版本號(hào)
uniqueId: methodMapping 文件上傳時(shí)時(shí)間戳,文件的唯一標(biāo)識(shí)
record: 數(shù)組數(shù)據(jù)
(3)數(shù)據(jù)存儲(chǔ)
服務(wù)端接收數(shù)據(jù)后,根據(jù)project、version、uniqueId,查詢出記錄的 record數(shù)據(jù),與上報(bào)的record 數(shù)據(jù) 做按位或計(jì)算,即 只要有1出現(xiàn),該位即為1,已覆蓋,再將結(jié)果存儲(chǔ)起來(lái)
所以覆蓋率數(shù)據(jù)都是按 uniqueId 進(jìn)行區(qū)分存儲(chǔ)的,即每個(gè)安裝包的覆蓋率數(shù)據(jù)都單獨(dú)存儲(chǔ)。
4、數(shù)據(jù)合并計(jì)算:
(1)兩個(gè)安裝包的覆蓋率數(shù)據(jù)如何合并
數(shù)據(jù)庫(kù)中存儲(chǔ)的覆蓋率數(shù)據(jù)是每個(gè)安裝包的數(shù)據(jù)以及基礎(chǔ)信息,當(dāng)兩個(gè)安裝包并不是同一個(gè)Tag,有代碼差異的時(shí)候,是不能直接進(jìn)行按位或計(jì)算合并數(shù)據(jù)的。
這時(shí) methodMapping文件就起到了作用。查找到兩個(gè)安裝包對(duì)應(yīng)的methodMapping文件,對(duì)其中的內(nèi)容進(jìn)行解析,就可以分別獲得兩個(gè)安裝包中的所有類(lèi)名、方法名、方法簽名等信息. 通過(guò)循環(huán)對(duì)比,即可進(jìn)行數(shù)據(jù)合并:
為了區(qū)分兩個(gè)不同Tag的安裝包,方便后續(xù)描述的理解,這里將Tag創(chuàng)建時(shí)間較早的安裝包稱(chēng)之為Old, 將Tag創(chuàng)建時(shí)間較晚的安裝包稱(chēng)之為New.
遍歷New 的Mapping數(shù)據(jù)
如果Old 的Mapping數(shù)據(jù)中?有相同的方法簽名,則方法內(nèi)部的所有邏輯分支進(jìn)行被執(zhí)行狀態(tài)的合并計(jì)算
如果Old 的Mapping數(shù)據(jù)中沒(méi)有相同的方法簽名,則認(rèn)為該方法為新增/修改過(guò)的方法,被執(zhí)行狀態(tài)以?New?的覆蓋率數(shù)據(jù)為準(zhǔn)
最終會(huì)得到以New 的代碼為準(zhǔn)的覆蓋率數(shù)據(jù),即相對(duì)較新代碼的整體覆蓋率數(shù)據(jù)
(2)要合并哪些安裝包
我們存儲(chǔ)了那么多安裝包的覆蓋率數(shù)據(jù),那么在實(shí)際的客戶端迭代流程中,我們應(yīng)該合并哪些安裝包的數(shù)據(jù),才可以幫助我們進(jìn)行分析,實(shí)現(xiàn)我們的方案目的呢?
我們對(duì)?單次打包緯度、單Tag緯度、版本緯度、分支緯度?幾種統(tǒng)計(jì)緯度進(jìn)行了利弊分析&對(duì)比,最后我們選擇使用分支緯度進(jìn)行覆蓋率數(shù)據(jù)的匯總統(tǒng)計(jì),即根據(jù)Tag的前后節(jié)點(diǎn)關(guān)系,合并同一分支線上的所有Tag(以前一版本的發(fā)版Tag為起點(diǎn),以當(dāng)前分支線中最新的Tag為終點(diǎn))的所有安裝包的覆蓋率數(shù)據(jù)。
?(3)多個(gè)安裝包的數(shù)據(jù)集如何快速合并
為了保證合并計(jì)算效率,采用多線程來(lái)處理。
以前一版本的發(fā)版Tag為起點(diǎn),以當(dāng)前分支線中最新的Tag為終點(diǎn),根據(jù)記錄的Tag前后節(jié)點(diǎn)關(guān)系,查詢出分支線中的Tag列表,再查詢出對(duì)應(yīng)的所有安裝包覆蓋率數(shù)據(jù)集
多線程分別合并數(shù)據(jù)集
流程
1、簡(jiǎn)化流程

2、打包流程

3、測(cè)試過(guò)程中上報(bào)流程

4、自動(dòng)計(jì)算流程

平臺(tái)功能
1、查看指定版本、分支的增量代碼覆蓋率數(shù)據(jù)?& 依賴(lài)組件工程的增量代碼覆蓋率數(shù)據(jù)

2、查看組件工程中詳細(xì)的類(lèi)增量覆蓋率數(shù)據(jù)
?3、查看類(lèi)增量代碼中邏輯分支的詳細(xì)覆蓋狀態(tài)?& 實(shí)時(shí)計(jì)算、渲染


4、選擇Tag區(qū)間,臨時(shí)計(jì)算增量覆蓋率數(shù)據(jù)
5、選擇同版本兩個(gè)Tag ,對(duì)比計(jì)算增量覆蓋率數(shù)據(jù)

后續(xù)規(guī)劃
分支統(tǒng)計(jì)緯度的優(yōu)缺點(diǎn)前面已經(jīng)提到過(guò),接下來(lái)我們會(huì)增加行覆蓋和方法覆蓋統(tǒng)計(jì)緯度,并且會(huì)結(jié)合Git diff,計(jì)算/渲染 Diff 代碼的覆蓋率數(shù)據(jù)。
現(xiàn)在iOS的插樁方案有些簡(jiǎn)單粗暴,效率也并不高,所以已經(jīng)開(kāi)始著手重構(gòu)。
我們也計(jì)劃在平臺(tái)上增加更多的人性化的功能,提升覆蓋率數(shù)據(jù)的分析效率、提升整體的使用體驗(yàn)。
好了,轉(zhuǎn)轉(zhuǎn)App代碼覆蓋率方案?就先給大家介紹的這里,希望能對(duì)大家有所幫助。
如果喜歡我們分享的內(nèi)容,歡迎點(diǎn)贊、在看、分享~

