通用技术 精准测试:基于 asm+javaparser 调用链差异化对比实践

tlhymm42 · 2020年05月29日 · 最后由 白开水pp 回复于 2023年08月09日 · 9552 次阅读

适用人群

如果你也面临这些问题

  1. 接触到的测试都是比较偏向底层,中台化的服务,对上层业务会比较陌生

  2. 日常开发提交测试点时会出现遗漏的情况,导致测试阶段漏测

  3. 开发测试比高,经常多个开发对一个测试,且日常发版频繁

  4. 想自己搞一套精准测试框架辅助测试

那么你可能也需要这么一套精准测试思路,帮助你精准且快速的进行日常测试

依赖技能树

在早几年前就了解到可以通过一些抽象语法解析工具或框架,针对 java 项目做链路梳理,再通过链路逆向反推测试回归点,趁着这个机会,较为深入的梳理了一下相关的知识体系

  1. 掌握:java 编程,了解 jvm 大致原理,特别编译阶段,类加载阶段

  2. 熟悉:asm,可将 class 文件梳理为一条完整的调用链

  3. 熟悉:javaparser,可将 java 文件解析为抽象语法树(AST)

对标前几年大肆推的 jacoco 用于精准测试,通过 AST 解析有以下优势

  1. jacoco 只告诉你执行过程中代码的哪些行没覆盖,具体这行有什么意义,为什么要覆盖,怎么去覆盖,这些你都无从得知;而通过调用链差异对比可更为精准的推送需要回归的业务
  2. jacoco 大多只能适用于单元测试,如果想集成测试使用还需要依赖 agent 注入;而通过调用链差异对比可通过接入 jenkins 在提测前就输出测试点,不需要改动业务代码
  3. 相比 jacoco 拿来就可以用,需要了解更多的 jvm 基础知识,同时扩充了个人知识体系

实践

  • 调用链扫描
  1. 加强 Class/Method 事件筛选器,保存父子方法调用关系
  2. 通过遍历特性分支编译后的 class 文件,再通过事件生成器启动,触发类/方法筛选器事件
  3. 最终只输出指定类型方法的调用链,包括:RPC 接口,HTTP 接口,定时任务,MQ 生产与消费

关键代码

1. asm构建classReader的方式不仅可以通过已加载的类名指定也可以通过输入流InputStream),这就使得通过直接遍历项目编译过的.class解析调用链变为可能
FileHelper.getFilePaths(classPath, dir, ".class"); // 遍历编译后的build/class路径下的所有.class文件
classPath.forEach(c->{
    ClassReader classReader = new ClassReader(new FileInputStream(c));
    ClassSpider classSpider = new ClassSpider(methodInvokeInfos);
    classReader.accept(classSpider, org.objectweb.asm.ClassReader.SKIP_FRAMES);
}

2. 扫描的目的是逆推对外暴露需要回归的功能例如接口定时器消息队列等所以需要排除掉一些无关的链路
例如dubbothriftjobnsq在编写中其类一般都或有特定的注解或有特定的父类或有特定实现的接口类型所以可以在类删选器classvisiotr中进行筛选
public AnnotationVisitor visitAnnotation(String annotation, boolean b) {
    if (annotation.endsWith("RestController;")) {
        flag = "HTTP"
    }
    return super.visitAnnotation(annotation, b);
}
  • 分支差异对比
  1. 遍历 master、branch 路径项目下的所有.java 文件,生成抽象语法树,并做去噪处理(空格,注释等无关改动)
  2. 对比方法(注解,签名,返回值,以及方法体),统计特性分支改动的方法

关键代码

1. 先比对有差异的文件这里直接比对文件大小以及是否存在新增的java文件收拢第二步的筛选范围
branch.forEach( (rp, b) -> {
    if (!master.containsKey(rp)) {
        b.setStatus(Status.NEW);
    } else {
        JavaFileInfo m = master.get(rp);
        if (b.getLength() != m.getLength()){
            b.setStatus(Status.MODIFY);
            m.setStatus(Status.MODIFY);
        }
    }
});
master.forEach( (rp, m) -> {
    if (!branch.containsKey(rp)) {
        m.setStatus(Status.DELETE);
    }
});

2. 遍历branch  master路径下修改过或新增的.java文件生成AST
CompilationUnit cu = StaticJavaParser.parse(file);
List<Comment> comments = cu.getAllContainedComments();  // 这里开始去除无关注释
        List<Comment> unwantedComments = comments  
                .stream()
                .filter(p -> !p.getCommentedNode().isPresent() || p instanceof LineComment)
                .collect(Collectors.toList());
        unwantedComments.forEach(Node::remove);
VoidVisitor<List<ClassParser>> classParserVoidVisitor = new VisitorPrinter();
classParserVoidVisitor.visit(cu, classParsers);  // 遍历文件,保存语法树

3. 对比差异化输出特性分支修改/新增的方法
masterMethod.checkAnnotationEqual(branchMethod).checkTypeEqual(branchMethod).checkBodyEqual(bbranchMethod);  // 这里我主要比对了方法的注解,详情就不展开了
  • 调用链&差异化输出
  1. 遍历调用链与上述差异化方法,输出需要回归的指定方法/接口
  2. 附带信息可包括:统计改动了多少行代码,改动的类型(包括:返回值改动,注解改动,新增方法,方法体变更),以及对应改动点
  • 调用链入库,并提供接口供查询 或 回调特定接口
  1. 可将接口与日常手工回归的案例/自动化案例做匹配,这样精准测试可以提送指定的案例用于回归&测试
共收到 12 条回复 时间 点赞

ASM 和 AST 在应用方面,对使用者的要求提高了几个级别。通常都是用在做代码 diff 和静态代码分析方面比较多一些,楼主用在覆盖率上可谓独辟蹊径。目前我们虽然做了对函数调用链的整理和存储,但是让测试同学反推用例明显感觉非常吃力

simple 回复

主要还是尝试用在精准测试不是覆盖率,通过调用链关系 + 代码 diff 去反推本次提测需要回归的测试点,具体的测试用例和对应测试点预期还是通过手工维护

仅楼主可见
仅楼主可见
仅楼主可见
仅楼主可见
仅楼主可见
test小生 回复

首先 VisitorPrinter extends VoidVisitorAdapter
如果你是想定位方法的起始、结尾行位置的话,重写 visit 方法,通过 range 获取 begin、end 或直接获取,都可以确定所在行范围

Barry250 一个菜鸡的精准测试实践 中提及了此贴 08月07日 16:19
仅楼主可见
tlhymm42 · #11 · 2021年08月25日 Author
仅楼主可见

可以开源么?

zlp 回复

自己实现了代码改动影响范围分析的工具,用 python 语言实现的,可以看看: https://github.com/baikaishuipp/jcci

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册