您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件测试技术 > 单元测试 > 正文

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

发表于:2024-04-01 作者:派大星 来源:码上遇见你

高手回答

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

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

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

常见的单元测试覆盖率统计工具包括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应用程序的运行时行为进行监控、调试、分析和优化等操作。举例来说,它可以用于实现性能监控、代码覆盖率检测、代码安全扫描等功能。

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

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