我们在进行单元测试时,经常需要关注一个覆盖率的指标,许多发布流程甚至要求达到特定的百分比。
那么,单元测试覆盖率是如何统计的呢?其底层实现原理又是怎样的呢?
单元测试覆盖率的统计原理实际上是通过字节码插桩实现的。也就是说,在编译期间会向代码中注入一些特殊的监控代码,以记录测试执行过程中代码的执行情况,从而推断代码的覆盖情况。这些监控代码能在运行时记录代码的执行情况,也能在编译时生成代码覆盖率报告。
常见的单元测试覆盖率统计工具包括JaCoCo、Emma、Cobertura等,这些工具能够在编译或运行时对代码进行插桩,并记录代码的执行情况,最终生成覆盖率报告。
具体见下表:
工具 | Jacoco | Emma | Cobertura |
原理 | 使用 ASM 修改字节码 | 修改 jar 文件,class 文件字节码文件 | 基于 jcoverage,基于 asm 框架对 class 文件插桩 |
覆盖粒度 | 行,类,方法,指令,分支 | 行,类,方法,基本块,指令,无分支覆盖 | 项目,包,类,方法的语句覆盖/分支覆盖 |
插桩 | on the fly、offline | on the fly、offline | offline,把统计代码插入编译好的class文件中 |
生成结果 | 在 Tomcat 的 catalina.sh 配置 javaangent 参数,指出需要收集覆盖率的文件,shutdown 时才收集,只能使用 kill 命令关闭 Tomcat,不要使用 kill -9 | html、xml、txt,二进制格式报表 | html,xml |
缺点 | 需要源代码 | 1、需要 debug 版本,并打来 build.xml 中的 debug 编译项;2、需要源代码,且必须与插桩的代码完全一致 | 1、不能捕获测试用例中未考虑的异常;2、关闭服务器才能输出覆盖率信息(已有修改源代码的解决方案,定时输出结果;输出结果之前设置了 hook,会与某些服务器的 hook 冲突,web 测试中需要将 cobertura.ser 文件来回 copy |
性能 | 快 | 小巧 | 插入的字节码信息更多 |
执行方式 | maven,ant,命令行 | 命令行 | maven,ant |
Jenkins 集成 | 生成 html 报告,直接与 hudson 集成,展示报告,无趋势图 | 无法与 hudson 集成 | 有集成的插件,美观的报告,有趋势图 |
报告实时性 | 默认关闭,可以动态从 jvm dump 出数据 | 可以不关闭服务器 | 默认是在关闭服务器时才写结果 |
维护状态 | 持续更新中 | 停止维护 | 停止维护,不支持java1.8的lamda表达式 |
Java字节码插桩技术是指在编译期或运行期,通过修改Java字节码的方式,在代码中插入额外的代码。这种技术可以在不改变Java源代码的情况下,对Java应用程序的运行时行为进行监控、调试、分析和优化等操作。举例来说,它可以用于实现性能监控、代码覆盖率检测、代码安全扫描等功能。
字节码插桩技术通常包括以下几个步骤:
假设我们希望对一个Java方法进行性能监控,我们可以在方法的入口和出口处分别插入计时器,以统计方法的执行时间。以下代码展示了如何实现这一功能:
public class Monitor { public static void start() { long startTime = System.nanoTime(); // 将起始时间记录到ThreadLocal中,以便在方法返回时进行计算 ThreadLocalHolder.set("startTime", startTime); } public static void end() { long endTime = System.nanoTime(); // 获取起始时间 long startTime = (long) ThreadLocalHolder.get("startTime"); // 计算方法执行时间 long elapsedTime = endTime - startTime; System.out.println("Method execution time: " + elapsedTime + "ns"); }}public class Example { public void method() { Monitor.start(); // 执行方法逻辑 Monitor.end(); }}
然而,若需监控多个方法的性能,分别在每个方法中插入Monitor.start()和Monitor.end()将导致代码重复、可读性下降,并存在遗漏的风险。在这种情况下,可以借助字节码插桩技术,在编译期或运行期间自动向每个方法的入口和出口处插入Monitor.start()和Monitor.end(),以确保代码的统一性和可维护性。
具体实现可借助字节码生成库ASM或Javassist来实现,此处以ASM为例。以下代码展示了如何使用ASM对Example类进行字节码插桩:
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); // 只为指定方法添加字节码插桩 if ("method".equals(name) && "()V".equals(desc)) { mv = new MethodVisitor(Opcodes.ASM5, mv) { @Override public void visitCode() { super.visitCode(); // 在方法执行之前插入字节码 mv.visitMethodInsn(INVOKESTATIC, "Monitor", "start", "()V", false); } @Override public void visitInsn(int opcode) { // 在方法返回之前插入字节码 if (opcode == RETURN) { mv.visitMethodInsn(INVOKESTATIC, "Monitor", "end", "()V", false); } super.visitInsn(opcode); } }; } return mv; } }; reader.accept(visitor, ClassReader.EXPAND_FRAMES); return writer.toByteArray(); }}
本文链接:http://www.28at.com/showinfo-26-80882-0.html你们单测覆盖率是如何统计的?原理是什么?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 深度解析Git核心机理,你学会了吗?
下一篇: 代码是如何被被编译的?