這篇文章寫得太好了,收藏,轉至https://blog.csdn.net/rr18758236029/article/details/109318224
文章結構
背景
Jacoco簡介
Jacoco 增量代碼覆蓋率設計方案
Jacoco增量代碼覆蓋率+持續交付
總結
一、背景
需求測試過程中,測試主要依靠需求及一些測試經驗來主觀保證質量。為了解決測試度量不清晰的問題,測試用jacoco從代碼層面衡量測試覆蓋率。分析未覆蓋部分代碼,從而反推前期測試設計是否重復,沒有覆蓋到的代碼是否是測試設計的盲點。代碼覆蓋率可以作為測試自我審視的重要工具之一。
jacoco代碼覆蓋率統計的是全量代碼覆蓋率,報告冗余,影響我們對報告的分析和查看。為了更精准衡量測試范圍和評估影響面,我們做了改造,使jacoco報告計算增量代碼覆蓋率
功能測試,接口自動化測試,單元測試都能計算到覆蓋率里面。支持通過悟空持續集成發布和預發環境手動部署兩種場景下的覆蓋率收集。
整體交互流程圖:
覆蓋率結果:
二、JaCoCo簡介
JaCoCo是一個開源的覆蓋率工具(官網地址:https://www.eclemma.org/jacoco/),針對的語言為java
工作步驟:
對Java字節碼進行插樁,有on-the-fly和offline兩種方式
執行測試用例,收集程序執行軌跡信息,支持通過dump講操作記錄從服務端傳輸到本地。
數據處理器結合程序執行軌跡信息和代碼結構信息分析生成代碼覆蓋率報告
結合源碼和編譯后的文件,可以將代碼覆蓋率報告圖形化展示出來,如html,xml等文件格式
經過比較,我們選擇的插樁模式是on-the-fly模式。該模式無需提前進行字節碼插樁,只需要JAVA_OPTS中增加-javaagent參數,該參數會被AgentOptions的getVMArgument方法加載。參數重制定jacocoAgent.jar文件,就可以在程序啟動時啟動Instrumentation的代理程序,代理程序再通過Class Loader裝載class前判斷是否轉換修改class文件將統計代碼插入class
三、JaCoCo增量代碼覆蓋率設計方案
JaCoCo增量代碼覆蓋率設計方案是基於JaCoCo做相應改造,生成我們所需要的覆蓋率數據。這里面主要需要解決的點在於獲取增量代碼並解析生成覆蓋率上。
改造可以拆分成以下幾個步驟:
獲取測試完成后的exec文件(二進制文件,里面歐探針的覆蓋執行信息)
獲取基線提交和被測提交之間的差異代碼
通過指定代碼倉庫名(project.key)和開發分支名(branch),解析開發分支和master之間的差異(數據需要精確到方法為度)
改造JaCoCo,是它支持僅對差異代碼生成覆蓋率報告
具體實現
1. 獲取增量數據
這部分涉及到對git的操作和對java文件的語法解析,主要用JGit和JavaPaser實現:JGit是一個用於操作git的Java庫,支持使用代碼操作git,支持我們獲得指定分支和master之間的差異,這里有一篇非常詳細的JGit介紹,感興趣的同學可以自行查閱http://qinghua.github.io/jgit/;JavaPaser是一個開源的Java語法解析庫https://github.com/javaparser,可以將java源碼解析為一顆語法樹,分析語法樹可以獲得Java代碼中的類,方法和方法入參等。
部分代碼片段如下:
private static List<DiffResult> getDiffResult (Repository repository, String projectName, String oldCommitSha, String newCommitSha) throws IOException, GitAPIException {
List<DiffResult> results = new ArrayList<>();
List<DiffEntry> list = diff4CommitOfJava(repository.getDirectory().getPath(),
oldCommitSha,
newCommitSha);
list.stream()
//過濾,只取add和modify的內容
.filter(diffEntry -> (DiffEntry.ChangeType.ADD == diffEntry.getChangeType() ||
DiffEntry.ChangeType.MODIFY == diffEntry.getChangeType()))
.forEach(diffEntry -> {
DiffResult diffResult = new DiffResult();
HashMap<String,List<Method>> changedMethods = MethodDiff.methodDiffInClass(BlobUtils.getContent(repository,ObjectId.fromString(oldCommitSha),diffEntry.getOldPath())
, BlobUtils.getContent(repository,ObjectId.fromString(newCommitSha),diffEntry.getNewPath()));
diffResult.setPath(diffEntry.getNewPath());
diffResult.setClassName(diffEntry.getNewPath().substring(diffEntry.getNewPath().lastIndexOf("/")+1,diffEntry.getNewPath().length()));
diffResult.setEntryType(diffEntry.getChangeType().toString());
try {
RevCommit oldCommit = new RevWalk(repository).parseCommit(ObjectId.fromString(oldCommitSha));
RevCommit newCommit = new RevWalk(repository).parseCommit(ObjectId.fromString(newCommitSha));
...
} catch (IOException e) {
LOGGER.error("commitSHA轉換為RevCommit失敗 {}",e);
}
diffResult.setNewId(diffEntry.getNewId().name());
diffResult.setOldId(diffEntry.getOldId().name());
diffResult.setChangeMethods(changedMethods);
results.add(diffResult);
});
return results;
}
2.改造JaCoCo
獲取到增量數據之后,需要對JaCoCo進行改造,主要包括agent,dump和report三個模塊
Agent
由於獲取增量數據需要額外兩個參數,需要擴展AgentOptions里可識別對參數,如下代碼塊,再VALID-OPTIONS里的參數都會被JaCoCo識別,可以增加自己需要的參數
private static final Collection<String> VALID_OPTIONS = Arrays.asList(
//在這里增加需要擴展的參數
DESTFILE, APPEND, INCLUDES, EXCLUDES, EXCLCLASSLOADER,
INCLBOOTSTRAPCLASSES, INCLNOLOCATIONCLASSES, SESSIONID, DUMPONEXIT,
OUTPUT, ADDRESS, PORT, CLASSDUMPDIR, JMX,PROJECT_KEY,BRANCH);
......
/**
* New instance parsed from the given option string.
*
* @param optionstr
* string to parse or <code>null</code>
*/
public AgentOptions(final String optionstr) {
this();
if (optionstr != null && optionstr.length() > 0) {
for (final String entry : OPTION_SPLIT.split(optionstr)) {
final int pos = entry.indexOf('=');
if (pos == -1) {
throw new IllegalArgumentException(format(
"Invalid agent option syntax \"%s\".", optionstr));
}
final String key = entry.substring(0, pos);
if (!VALID_OPTIONS.contains(key)) {
throw new IllegalArgumentException(format(
"Unknown agent option \"%s\".", key));
}
final String value = entry.substring(pos + 1);
setOption(key, value);
}
validateAll();
}
}
Dump
在Dump數據時需要增加一步,獲取增量數據
...
try {
//該處hookfor增量代碼覆蓋率統計時需要
ExtraData.setAddedData(key, targetBranch);
final ExecFileLoader loader = client.dump(address, port);
if (dump) {
log(format("Dumping execution data to %s",
destfile.getAbsolutePath()));
loader.save(destfile, append);
}
} catch (final IOException e) {
throw new BuildException("Unable to dump coverage data", e,
getLocation());
}
...
/**
* 向diff接口請求增量數據
*/
public class ExtraData {
public static List<JacocoClass> ADD_DATA;
public static void setAddedData(String key,String targetBranch) throws IOException {
String url = String.format("http://qc.beibei.com.cn/api/gitDiff/jacocoClass?appKey=XXX&key=%s&targetBranch=%s",key,targetBranch);
List<JacocoClass> addedData = HttpUtil.get4Jacoco(url);
ADD_DATA = addedData;
}
}
Report
在生成報告時只對增量數據做處理,由此生成增量覆蓋率報告。JaCoCo對exec文件的解析主要是在Analyzer類的analyzeClass(final byte[] source)方法,這里面會調用createAnalyzingVisitor方法,生成一個用於解析的ASM類訪問器,核心代碼為下
/**
* Analyzes the class given as a ASM reader.
*
* @param reader reader with class definitions
*/
public void analyzeClass(final ClassReader reader) {
//僅增量的類才會采集覆蓋數據
if (null == ExtraData.ADD_DATA) {
final ClassVisitor visitor = createAnalyzingVisitor(
CRC64.classId(reader.b), reader.getClassName());
reader.accept(visitor, 0);
}else if (HookUtil.isHookedClass(reader.getClassName(), ExtraData.ADD_DATA)) {
final ClassVisitor visitor = createAnalyzingVisitor(
CRC64.classId(reader.b), reader.getClassName());
reader.accept(visitor, 0);
}
}
對方法級別的探針計算邏輯是在ClassProbesAdater類的visitMethod方法里面,我們只需要改造visitMethod方法,使它只對提取出的每個類的新增或變更方法做解析,非指定累和方法不做處理,核心代碼如下:
@Override
public final MethodVisitor visitMethod(final int access, final String name,
final String desc, final String signature, final String[] exceptions) {
//無增量數據,不做增量覆蓋率統計操作
if (null == ExtraData.ADD_DATA) {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
//當前方法為增量變更方法,需要做覆蓋率統計操作
else if (HookUtil.isHookedMethod(this.name,name, MethodUtil.getDesc4Diff(desc), ExtraData.ADD_DATA,"")) {
System.out.println("hookedMethod:"+name);
final MethodProbesVisitor methodProbes;
final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
signature, exceptions);
if (mv == null) {
// We need to visit the method in any case, otherwise probe ids
// are not reproducible
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);
methodProbes.accept(this, analyzer);
} else {
methodProbes.accept(this, probesAdapter);
}
}
};
} else {
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
3.同一個分支多次部署報告合並
在實際使用過程中,開發往往會在同個分支上頻繁修改代碼並多次部署,會生成多分報告,不幸的是,JaCoCo官方提供的merge功能,針對的是兩份源碼相同生成的報告,開發代碼有變更無法合並,也無法統計處最終的覆蓋率數據,可用率大大下降
針對該問題,解決思路是:
V1版本的代碼部署測試后生成增量報告R1,更新后的代碼版本V2部署測試后生成增量報告R2,使用Diff服務查找V1和V2之間的差異,記為D
我們認為,如果是V1和V2沒有改動的部分,如果V1覆蓋到了,即使V2中沒有覆蓋,也認為是被覆蓋的,因此,統計R2中覆蓋的部分F2,同時去除不在D中的部分,記為F2‘。
將R2和F2’合並,修改R2中美覆蓋但是F2‘中覆蓋到的數據,生成最終覆蓋率報告。
這部分腳本是python實現的,使用爬蟲框架Beautiful Soup https://cuiqingcai.com/1319.html,部分核心代碼如下:
def filterFc(fcpc, diffResult):
"""
:param fcpc: .java.html中的fc/pc的行
:param diffResult: diff結果
:return: 不在diff結果里的fc/pc
"""
for fpc in fcpc:
for diffRe in diffResult:
if fpc['path'].strip('.html') == diffRe['path'] and diffRe[
'beginLine'] < int(fpc['line'].strip('L')) < diffRe['endLine']:
break
jafc.append(fpc)
break
return jafc
def modifyHtml(jafc, path, path_new):
"""
修改當前報告中特定的行
:param jafc: 不在diff結果里的fc/pc
:param path: 前一次報告路徑
:param path_new: 當前報告路徑
"""
total = 0
for jaf in jafc:
if os.path.exists(jaf['path'].replace(path, path_new)):
soup_old = BeautifulSoup(open(jaf['path']), features="lxml")
findSpan_old = soup_old.find(attrs={'id': jaf['line']})
soup = BeautifulSoup(open(jaf['path'].replace(path, path_new)), features="lxml")
findSpan = soup.find('span', text=findSpan_old.text)
if findSpan is not None:
if findSpan['class'] == [u'nc']:
findSpan['class'] = [u'fc']
print findSpan
total = total + 1
html = soup.prettify(soup.original_encoding)
with open(jaf['path'].replace(path, path_new), "wb") as file:
file.write(html)
print "total:" + str(total)
return total
有了不同代碼版本的合並功能后,我們在每次生成報告后,都拿相同開發分支且更新時間降序排列第二(第一是當前這份報告)的報告進行合並,最終生成合並后的總覆蓋率報告。
4.效果
至此,我們就可以獲得增量代碼覆蓋率報告,報告只會對變更的方法進行覆蓋率統計,效果如圖,包括從包、類、方法、代碼各個級別的報告:
5.報告解讀
首先我們需要知道報告中各種標記的含義,代碼的背景顏色代表這一行的行覆蓋情況:
紅色背景:這一行沒有任何指令被執行;
黃色背景:這一行有部分指令被執行;
綠色背景:這一行的所有指令都被執行了;
代碼前方的菱形則代表這一行的分支覆蓋情況,在if-else或者switch等邏輯分支語句前會出現:
紅色菱形:這一行沒有分支被執行;
黃色菱形:這一行有部分分支被執行;
綠色菱形:這一行的所有分支都被執行了;
(1)精確定位代碼邏輯錯誤
如何根據JaCoCo報告快速並且准確的定位問題?舉個栗子:
這是一個判斷是否是粉絲的代碼片段,在兩個判斷語句都為真的情況下為真。在功能測試時,測試了粉絲和非粉絲的場景,生成的報告結果如上圖。顯然,這個分支語句始終沒有完全覆蓋,並且,&&后半段的分支是完全沒有跑到的。為什么呢?簡單的推理一下,一定是&&前半部分的代碼邏輯錯誤(第一行),導致這部分判斷結果一直為false,結合&&的短路原理,&&后半部分的邏輯才會一直走不進去。由此,我們可以快速定位到是第一行代碼有bug。
(2)快速發現測試遺漏or冗余代碼
在測試過程中,出現大量代碼未覆蓋的情況怎么辦?很簡單,去找開發了解這部分的代碼邏輯:如果是測試遺漏的,反向補充用例,有目標的查漏補缺;如果是廢棄代碼,則提醒開發刪除,有效去除項目中的冗余代碼。
四、JaCoCo增量代碼覆蓋率+持續交付
目前我們的增量代碼覆蓋率工具已經集成到持續交付平台 Wukong 和測試平台 qc。
具體運作方式:
接入Wukong的應用,在部署時就會初始化一條數據記入db,這條記錄包含了部署的必要信息,初始狀態為“等待測試”;同時,在JaCoCo專屬的服務器上也會開始執行拉代碼、編譯等操作,為后續生成報告做准備。
部署之后,在測試過程中,點擊刷新報告,在JaCoCo專屬的服務器上開始運行生成增量覆蓋率報告任務;在任務結束后會將報告地址寫入db,同時釘釘通知報告生成狀態和報告地址,此時報告狀態被更新為“測試中”。
在Wukong上取消發布/驗證通過后,自動再次運行生成增量覆蓋率報告任務,更新報告狀態為“測試完成”,防止在部署過程中未點擊過刷新報告的數據被遺漏。
qc平台覆蓋率結果一覽:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-k1kxKEln-1603796132232)(/Users/chenxiwen/Library/Application Support/typora-user-images/image-20201027185501049.png)]
五、總結
至此,我們的JaCoCo+持續交付流程已經非常完善了:
接入過程開發無感知,不需要修改代碼,降低推廣難度;
測試過程中如果需要,也支持多次更新報告;
多次部署,合並為一份報告,方便后續進行測試分析;
整個流程全自動收集,即使是沒有測試參與的項目,覆蓋率數據也盡在掌握。
參考資料:jacoco官網
————————————————
版權聲明:本文為CSDN博主「咕咕榮」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/rr18758236029/article/details/109318224