写点什么

你们单测覆盖率是如何统计的?原理是什么?

作者:派大星
  • 2024-04-02
    辽宁
  • 本文字数:2256 字

    阅读完需:约 7 分钟

高手回答

我们在进行单元测试时,经常需要关注一个覆盖率的指标,许多发布流程甚至要求达到特定的百分比。


那么,单元测试覆盖率是如何统计的呢?其底层实现原理又是怎样的呢?


单元测试覆盖率的统计原理实际上是通过字节码插桩实现的。也就是说,在编译期间会向代码中注入一些特殊的监控代码,以记录测试执行过程中代码的执行情况,从而推断代码的覆盖情况。这些监控代码能在运行时记录代码的执行情况,也能在编译时生成代码覆盖率报告。


常见的单元测试覆盖率统计工具包括 JaCoCoEmmaCobertura 等,这些工具能够在编译或运行时对代码进行插桩,并记录代码的执行情况,最终生成覆盖率报告。


具体见下表:


什么是字节码插桩

Java 字节码插桩技术是指在编译期或运行期,通过修改 Java 字节码的方式,在代码中插入额外的代码。这种技术可以在不改变 Java 源代码的情况下,对 Java 应用程序的运行时行为进行监控、调试、分析和优化等操作。举例来说,它可以用于实现性能监控、代码覆盖率检测、代码安全扫描等功能。


字节码插桩技术通常包括以下几个步骤:


  1. 生成目标类的字节码,这一步可以通过 Java 编译器(如 javac)或其他工具(如 AspectJ)来完成。

  2. 解析字节码,识别需要进行插桩的代码区域(如方法、循环、异常处理等)。

  3. 插入额外的字节码,通常通过编写 Java 代码来实现这一步,然后利用字节码生成库(如 ASM、Javassist 等)生成相应的字节码。

  4. 将修改后的字节码重新写回到磁盘或内存中,以供后续使用。


假设我们希望对一个 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(); }}
复制代码


如有问题,欢迎加微信交流:w714771310,备注- 技术交流  。或微信搜索【码上遇见你】。


免费的Chat GPT可微信搜索【AI贝塔】进行体验,无限使用。


好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

发布于: 刚刚阅读数: 4
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
你们单测覆盖率是如何统计的?原理是什么?_测试 单元测试_派大星_InfoQ写作社区