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

典型回答

我们在跑单元测试的时候,经常会有一个覆盖率的指标,甚至很多发布过程要求覆盖率一定要达到一定的百分比。

那么,单测覆盖率是如何统计出来的呢?底层的实现原理是什么呢?

单测覆盖率的统计原理其实是通过字节码插桩实现的, 即在编译时在代码中插入一些特殊的监控代码,来记录测试执行过程中代码的执行情况,进而得出代码的覆盖情况。这些监控代码可以在运行时记录代码被执行的情况,也可以在编译时生成代码覆盖率报告。

常见的单测覆盖率统计工具有JaCoCo、Emma、Cobertura等,这些工具可以在编译时或者运行时对代码进行插桩,记录代码的执行情况,并生成覆盖率报告。

扩展知识

字节码插桩

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();
    }
}

原文: https://www.yuque.com/hollis666/xkm7k3/df5wved1tx1wag6n