你們單測(cè)覆蓋率是如何統(tǒng)計(jì)的?原理是什么?
高手回答
我們?cè)谶M(jìn)行單元測(cè)試時(shí),經(jīng)常需要關(guān)注一個(gè)覆蓋率的指標(biāo),許多發(fā)布流程甚至要求達(dá)到特定的百分比。
那么,單元測(cè)試覆蓋率是如何統(tǒng)計(jì)的呢?其底層實(shí)現(xiàn)原理又是怎樣的呢?
單元測(cè)試覆蓋率的統(tǒng)計(jì)原理實(shí)際上是通過字節(jié)碼插樁實(shí)現(xiàn)的。也就是說,在編譯期間會(huì)向代碼中注入一些特殊的監(jiān)控代碼,以記錄測(cè)試執(zhí)行過程中代碼的執(zhí)行情況,從而推斷代碼的覆蓋情況。這些監(jiān)控代碼能在運(yùn)行時(shí)記錄代碼的執(zhí)行情況,也能在編譯時(shí)生成代碼覆蓋率報(bào)告。
常見的單元測(cè)試覆蓋率統(tǒng)計(jì)工具包括JaCoCo、Emma、Cobertura等,這些工具能夠在編譯或運(yùn)行時(shí)對(duì)代碼進(jìn)行插樁,并記錄代碼的執(zhí)行情況,最終生成覆蓋率報(bào)告。
具體見下表:
工具 | Jacoco | Emma | Cobertura |
原理 | 使用 ASM 修改字節(jié)碼 | 修改 jar 文件,class 文件字節(jié)碼文件 | 基于 jcoverage,基于 asm 框架對(duì) class 文件插樁 |
覆蓋粒度 | 行,類,方法,指令,分支 | 行,類,方法,基本塊,指令,無分支覆蓋 | 項(xiàng)目,包,類,方法的語句覆蓋/分支覆蓋 |
插樁 | on the fly、offline | on the fly、offline | offline,把統(tǒng)計(jì)代碼插入編譯好的class文件中 |
生成結(jié)果 | 在 Tomcat 的 catalina.sh 配置 javaangent 參數(shù),指出需要收集覆蓋率的文件,shutdown 時(shí)才收集,只能使用 kill 命令關(guān)閉 Tomcat,不要使用 kill -9 | html、xml、txt,二進(jìn)制格式報(bào)表 | html,xml |
缺點(diǎn) | 需要源代碼 | 1、需要 debug 版本,并打來 build.xml 中的 debug 編譯項(xiàng);2、需要源代碼,且必須與插樁的代碼完全一致 | 1、不能捕獲測(cè)試用例中未考慮的異常;2、關(guān)閉服務(wù)器才能輸出覆蓋率信息(已有修改源代碼的解決方案,定時(shí)輸出結(jié)果;輸出結(jié)果之前設(shè)置了 hook,會(huì)與某些服務(wù)器的 hook 沖突,web 測(cè)試中需要將 cobertura.ser 文件來回 copy |
性能 | 快 | 小巧 | 插入的字節(jié)碼信息更多 |
執(zhí)行方式 | maven,ant,命令行 | 命令行 | maven,ant |
Jenkins 集成 | 生成 html 報(bào)告,直接與 hudson 集成,展示報(bào)告,無趨勢(shì)圖 | 無法與 hudson 集成 | 有集成的插件,美觀的報(bào)告,有趨勢(shì)圖 |
報(bào)告實(shí)時(shí)性 | 默認(rèn)關(guān)閉,可以動(dòng)態(tài)從 jvm dump 出數(shù)據(jù) | 可以不關(guān)閉服務(wù)器 | 默認(rèn)是在關(guān)閉服務(wù)器時(shí)才寫結(jié)果 |
維護(hù)狀態(tài) | 持續(xù)更新中 | 停止維護(hù) | 停止維護(hù),不支持java1.8的lamda表達(dá)式 |
什么是字節(jié)碼插樁
Java字節(jié)碼插樁技術(shù)是指在編譯期或運(yùn)行期,通過修改Java字節(jié)碼的方式,在代碼中插入額外的代碼。這種技術(shù)可以在不改變Java源代碼的情況下,對(duì)Java應(yīng)用程序的運(yùn)行時(shí)行為進(jìn)行監(jiān)控、調(diào)試、分析和優(yōu)化等操作。舉例來說,它可以用于實(shí)現(xiàn)性能監(jiān)控、代碼覆蓋率檢測(cè)、代碼安全掃描等功能。
字節(jié)碼插樁技術(shù)通常包括以下幾個(gè)步驟:
- 生成目標(biāo)類的字節(jié)碼,這一步可以通過Java編譯器(如javac)或其他工具(如AspectJ)來完成。
- 解析字節(jié)碼,識(shí)別需要進(jìn)行插樁的代碼區(qū)域(如方法、循環(huán)、異常處理等)。
- 插入額外的字節(jié)碼,通常通過編寫Java代碼來實(shí)現(xiàn)這一步,然后利用字節(jié)碼生成庫(如ASM、Javassist等)生成相應(yīng)的字節(jié)碼。
- 將修改后的字節(jié)碼重新寫回到磁盤或內(nèi)存中,以供后續(xù)使用。
假設(shè)我們希望對(duì)一個(gè)Java方法進(jìn)行性能監(jiān)控,我們可以在方法的入口和出口處分別插入計(jì)時(shí)器,以統(tǒng)計(jì)方法的執(zhí)行時(shí)間。以下代碼展示了如何實(shí)現(xiàn)這一功能:
public class Monitor {
public static void start() {
long startTime = System.nanoTime();
// 將起始時(shí)間記錄到ThreadLocal中,以便在方法返回時(shí)進(jìn)行計(jì)算
ThreadLocalHolder.set("startTime", startTime);
}
public static void end() {
long endTime = System.nanoTime();
// 獲取起始時(shí)間
long startTime = (long) ThreadLocalHolder.get("startTime");
// 計(jì)算方法執(zhí)行時(shí)間
long elapsedTime = endTime - startTime;
System.out.println("Method execution time: " + elapsedTime + "ns");
}
}
public class Example {
public void method() {
Monitor.start();
// 執(zhí)行方法邏輯
Monitor.end();
}
}
然而,若需監(jiān)控多個(gè)方法的性能,分別在每個(gè)方法中插入Monitor.start()和Monitor.end()將導(dǎo)致代碼重復(fù)、可讀性下降,并存在遺漏的風(fēng)險(xiǎn)。在這種情況下,可以借助字節(jié)碼插樁技術(shù),在編譯期或運(yùn)行期間自動(dòng)向每個(gè)方法的入口和出口處插入Monitor.start()和Monitor.end(),以確保代碼的統(tǒng)一性和可維護(hù)性。
具體實(shí)現(xiàn)可借助字節(jié)碼生成庫ASM或Javassist來實(shí)現(xiàn),此處以ASM為例。以下代碼展示了如何使用ASM對(duì)Example類進(jìn)行字節(jié)碼插樁:
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.io.IOException;
public class MonitorTransformer implements Opcodes {
public static byte[] transform(byte[] classBytes) throws IOException {
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
// 只為指定方法添加字節(jié)碼插樁
if ("method".equals(name) && "()V".equals(desc)) {
mv = new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitCode() {
super.visitCode();
// 在方法執(zhí)行之前插入字節(jié)碼
mv.visitMethodInsn(INVOKESTATIC, "Monitor", "start", "()V", false);
}
@Override
public void visitInsn(int opcode) {
// 在方法返回之前插入字節(jié)碼
if (opcode == RETURN) {
mv.visitMethodInsn(INVOKESTATIC, "Monitor", "end", "()V", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
};
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
}