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

高手回答

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

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

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

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

具体见下表:

工具 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方法进行性能监控,我们可以在方法的入口和出口处分别插入计时器,以统计方法的执行时间。以下代码展示了如何实现这一功能:

csharp 复制代码
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类进行字节码插桩:

java 复制代码
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贝塔】进行体验,无限使用。

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

相关推荐
Chrikk22 分钟前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*25 分钟前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue25 分钟前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man28 分钟前
【go从零单排】go语言中的指针
开发语言·后端·golang
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
Yaml43 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
测试19984 小时前
2024软件测试面试热点问题
自动化测试·软件测试·python·测试工具·面试·职场和发展·压力测试
小码编匠4 小时前
一款 C# 编写的神经网络计算图框架
后端·神经网络·c#