适用人群

如果你也面临这些问题

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

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

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

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

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

依赖技能树

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

掌握:java 编程,了解 jvm 大致原理,特别编译阶段,类加载阶段 熟悉:asm,可将 class 文件梳理为一条完整的调用链 熟悉:javaparser,可将 java 文件解析为抽象语法树(AST)

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

jacoco 只告诉你执行过程中代码的哪些行没覆盖,具体这行有什么意义,为什么要覆盖,怎么去覆盖,这些你都无从得知;而通过调用链差异对比可更为精准的推送需要回归的业务

jacoco 大多只能适用于单元测试,如果想集成测试使用还需要依赖 agent 注入;而通过调用链差异对比可通过接入 jenkins 在提测前就输出测试点,不需要改动业务代码

相比 jacoco 拿来就可以用,需要了解更多的 jvm 基础知识,同时扩充了个人知识体系

实践

调用链扫描

加强 Class/Method 事件筛选器,保存父子方法调用关系

通过遍历特性分支编译后的 class 文件,再通过事件生成器启动,触发类/方法筛选器事件

最终只输出指定类型方法的调用链,包括: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. 扫描的目的是逆推对外暴露需要回归的功能,例如接口,定时器,消息队列等,所以需要排除掉一些无关的链路
//例如dubbo,thrift,job,nsq在编写中其类一般都或有特定的注解,或有特定的父类,或有特定实现的接口类型,所以可以在类删选器classvisiotr中进行筛选
public AnnotationVisitor visitAnnotation(String annotation, boolean b) {
    if (annotation.endsWith("RestController;")) {
        flag = "HTTP"
    }
    return super.visitAnnotation(annotation, b);
}

分支差异对比

遍历 master、branch 路径项目下的所有.java 文件,生成抽象语法树,并做去噪处理(空格,注释等无关改动)

对比方法(注解,签名,返回值,以及方法体),统计特性分支改动的方法

关键代码

//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);  // 这里我主要比对了方法的注解,详情就不展开了

调用链&差异化输出

遍历调用链与上述差异化方法,输出需要回归的指定方法/接口

附带信息可包括:统计改动了多少行代码,改动的类型(包括:返回值改动,注解改动,新增方法,方法体变更),以及对应改动点

调用链入库,并提供接口供查询 或 回调特定接口

可将接口与日常手工回归的案例/自动化案例做匹配,这样精准测试可以提送指定的案例用于回归&测试

参考资料

https://testerhome.com/topics/23819