专栏名称: 美团技术团队
10000+工程师,如何支撑中国领先的生活服务电子商务平台?数亿消费者、数百万商户、2000多个行业、几千亿交易额背后是哪些技术在支撑?这里是美团、大众点评、美团外卖、美团配送、美团优选等技术团队的对外窗口。
目录
相关文章推荐
51好读  ›  专栏  ›  美团技术团队

Android增量代码测试覆盖率工具

美团技术团队  · 公众号  · 架构  · 2017-06-16 19:36

正文

请到「今天看啥」查看全文



生成代码的覆盖率报告,首先想到的就是 JaCoCo ,下面分别介绍一下JaCoCo的原理和我们所做的改造。


JaCoCo概述


JaCoCo包含了多种维度的 覆盖率计数器 :指令级计数器(C0 coverage)、分支级计数器(C1 coverage)、圈复杂度、行覆盖、方法覆盖、类覆盖。其覆盖率报告的示例如下:



  • 绿色:表示行覆盖充分。

  • 红色:表示未覆盖的行。

  • 黄色棱形:表示分支覆盖不全。

  • 绿色棱形:表示分支覆盖完全。


注入原理


JaCoCo主要通过代码注入的方式来实现上面覆盖率的功能。JaCoCo支持的注入方式如下图(图片出自 这里 )所示:



包含了几种不同的收集覆盖率信息的方法,每个方法的实现都不太一样,这里主要关心字节码注入这种方式(Byte Code)。Byte Code包含Offline和On-The-Fly两种注入方式:

  • Offline:在生成最终的目标文件之前,对Class文件进行插桩,生成最终的目标文件,执行目标文件以后得到覆盖执行结果,最终生成覆盖率报告。

  • On-The-Fly:JVM通过-javaagent指定特定的Jar来启动Instrumentation代理程序,代理程序在ClassLoader装载一个class前先判断是否需要对class进行注入,对于需要注入的class进行注入。覆盖率结果可以在JVM执行代码的过程中完成。


可以看到,On-The-Fly因为要修改JVM参数,所以对环境的要求比较高,为了屏蔽工具对虚拟机环境的依赖,我们的代码注入主要选择Offline这种方式。

Offline的工作流程:

  1. 在生成最终目标文件之前对字节码进行插桩。

  2. 运行测试代码,得到运行时数据。

  3. 根据运行时数据、生成的class文件、源码生成覆盖率报告。


通过一张图来形象地表示一下:



如何实现代码注入呢?举个例子说明一下:



JaCoCo通过ASM在字节码中插入Probe指针(探测指针),每个探测指针都是一个BOOL变量(true表示执行、false表示没有执行),程序运行时通过改变指针的结果来检测代码的执行情况(不会改变原代码的行为)。探测指针完整插入策略请参考 Probe Insertion Strategy


增量注入


介绍完JaCoCo注入原理以后,我们来看看如何做到增量注入:

JaCoCo默认的注入方式为全量注入。通过阅读源码,发现注入的逻辑主要在ClassProbesAdapter中。ASM在遍历字节码时,每次访问一个方法定义,都会回调这个类的visitMethod方法,在visitMethod方法中再调用ClassProbeVisitor的visitMethod方法,并最终调用MethodInstrumenter完成注入。部分代码片段如下:

@Overridepublic final MethodVisitor visitMethod(final int access, final String name,      final String desc, final String signature, final String[] exceptions) {   final MethodProbesVisitor methodProbes;   final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
         signature, exceptions);   if (mv == null) {
      methodProbes = EMPTY_METHOD_PROBES_VISITOR;
   } else {
      methodProbes = mv;
   }   return new MethodSanitizer(null, access, name, desc, signature,
         exceptions) {      @Override
      public void visitEnd() {         super.visitEnd();
         LabelFlowAnalyzer.markLabels(this);         final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
               methodProbes, ClassProbesAdapter.this);         if (trackFrames) {            final AnalyzerAdapter analyzer = new AnalyzerAdapter(
                  ClassProbesAdapter.this.name, access, name, desc,
                  probesAdapter);
            probesAdapter.setAnalyzer(analyzer);            this.accept(analyzer);
         } else {            this.accept(probesAdapter);
         }
      }
   };
}


看到这里基本上已经知道如何去修改JaCoCo的源码了。继承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只对变化了方法进行注入:

@Overridepublic final MethodVisitor visitMethod(final int access, final String name,                                       final String desc, final String signature, final String[] exceptions) {    if (Utils.shoudHackMethod(name,desc,signature,changedMethods,cv.getClassName())) {
        ...
    } else {        return  cv.getCv().visitMethod(access, name, desc, signature, exceptions);
    }
}


生成增量代码的覆盖率报告


和增量注入的原理类似,通过阅读源码,分别需要修改Analyzer(只对变化的类做处理):

@Overridepublic void analyzeClass(final ClassReader reader) {  if (Utils.shoudHackMethod(reader.getClassName(),changedMethods)) {
        ...
    } 
}


和ReportClassProbesAdapter(只对变化的方法做处理):

@Override






请到「今天看啥」查看全文