jacoco测试覆盖率

测试覆盖率

测试覆盖率是对测试完全程度的评测。测试覆盖率是由测试需求和测试用例的覆盖或已执行代码的覆盖的表示结果。一方面可以衡量测试工作本身的有效性,提升测试效率,一方面可以提升代码质量,减少bug,提升产品的可靠性,稳定性。

代码覆盖率的意义

  • 分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
  • 检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
  • 代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。

技术框架选型

屏幕快照 2019-08-09 下午5.15.36.png

jacoco

接入

引入依赖

#project build.gradle

allprojects {
    repositories {
            maven { url 'https://jitpack.io' }
        }
    }
    
#module build.gradle
    dependencies {
           compile 'com.github.Jay-Goo:JacocoTestHelper:v0.0.2'
    }

引入脚本配置,jacoco.gradle这个脚本在文章末尾的参考可以找到

apply from: 'jacoco.gradle'
android {
 buildTypes {
        release {
           ...
        }
        debug {
            /**打开覆盖率统计开关*/
            testCoverageEnabled = true
        }
    }
}

初始化与测试报告的生成

//PROJECT_PATH '项目路径' + '/app/build/outputs/code-coverage/'
//初始化,可以在程序入口处初始化
JacocoHelper.init(PROJECT_PATH,true);

//生成代码覆盖率ec文件
//可以手动触发,也可以在程序退出时触发
JacocoHelper.generateEcFile(true);

使用流程

  • 1.执行./gradlew jacocoInit 初始化工程
  • 2.正常使用测试,生成测试报告(demo中是通过按钮生成测试报告,根据场景自行设置)
  • 3.测试结束后通过adb命令拉出sdcard目录中的ec文件
  • 4.放入到/app/build/outputs/code-coverage/目录下(目录可以自定义)
  • 5.执行./gradlew jacocoTestReport
  • 6.然后到 /app/build/reports里查看相关报告

测试效果

屏幕快照 2019-08-09 下午5.02.57.png
屏幕快照 2019-08-09 下午5.03.04.png

增量测试覆盖率

对于增量代码的定义

工程中,由于我们的开发方式是每个人新建自己的开发分支进行开发,最后合并到主分支上,所以我们对增量代码的定义是当前分支与指定分支的diff结果

增量代码的实现方式

jacoco本身不支持输出增量测试覆盖率报告,想实现增量代码的技术方案基本有两种

  • 修改jacoco源代码
  • 脚本工具diff-cover与jacoco配合使用,来完成增量测试覆盖率的实现

方案1短期成本比较高,可以使用方案2先做一个过度,并且提前验证jacoco的准确性,满足业务需要。并且根据使用过程中产生问题或者测试的建议,之后开始修改jacoco的源代码,简化流程,提高效率

diff-cover的引入

环境变量配置参考
https://github.com/Bachmann1234/diff_cover
不支持window系统

使用

与前边使用过程一样,只不过./gradlew jacocoTestReport命令需要替换为如下命令

diff-cover jacoco报告生成的位置xml格式 --html-report 你的报告生成的位置—compare-branch origin/release-8.24.0 --src-roots /源代码路径

测试效果

屏幕快照 2019-08-09 下午5.02.39.png

测试报告解读

JaCoCo包含了多种维度的覆盖率计数器,具体如下

  • 行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
  • 类覆盖率:度量计算class类文件是否被执行。
  • 分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量。
  • 方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
  • 指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
  • 圈复杂度:在线性组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测 试案例没有完全覆盖到这个模块。

如图,覆盖率计数器与测试报告一一对应


屏幕快照 2019-08-09 下午6.12.27.png

代码视图报告


屏幕快照 2019-08-09 下午6.05.13.png
  • 绿色:表示行覆盖充分。
  • 红色:表示未覆盖的行。
  • 黄色棱形:表示分支覆盖不全。
  • 绿色棱形:表示分支覆盖完全。

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

Offline的工作流程:

  • 在生成最终目标文件之前对字节码进行插桩。
  • 运行测试代码,得到运行时数据。
  • 根据运行时数据、生成的class文件、源码生成覆盖率报告。

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

插入前源码

屏幕快照 2019-08-19 上午10.06.23.png

插入后源码

屏幕快照 2019-08-19 上午10.06.09.png

插桩策略

对于代码

public static void example() {
    a();
    if (cond()) {
        b();
    } else {
        c();
    }
    d();
}

编译后转换为字节码:方法内有自己的插入,主要在条件语句中插入

public static example()V
      INVOKESTATIC a()V
      INVOKESTATIC cond()Z
      IFEQ L1
      INVOKESTATIC b()V
      GOTO L2
  L1: INVOKESTATIC c()V
  L2: INVOKESTATIC d()V
      RETURN
屏幕快照 2019-08-19 上午10.15.00.png

由Java字节码定义的控制流图有不同的类型,每个类型连接一个源指令和一个目标指令,当然有时候源指令和目标指令并不存在,或者无法被明确(异常)。不同类型的插入策略也是不一样的


屏幕快照 2019-08-19 上午10.18.12.png
屏幕快照 2019-08-19 上午10.33.34.png

JaCoCo是用一个布尔数组来实现探针,每个探针对应于该数组中的项。当以下四个字节码指令触发时探针进行输入设置为true


屏幕快照 2019-08-19 上午10.37.51.png

JaCoCo对行探针是这样处理的,添加两行指令之间的一个额外的探针时,后续行至少包含一个方法调用。

源码分析

在插桩中,程序入口是org.jacoco.ant.InstrumentTask,向其传入了两个参数destdir和fileset,分别是存放插入后的字节码文件位置以及字节码文件。

在InstrumentTask类中,由于是自定义gradle Task,所以执行函数是excute(),在instrument()函数中调用Instrumenter类,在instrument(final ClassReader reader)函数中,有以下代码:

final ClassWriter writer = new ClassWriter(reader, 0);

final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
                .createFor(reader, accessorGenerator);

final ClassVisitor visitor = new ClassProbesAdapter(
                new ClassInstrumenter(strategy, writer), true);

reader.accept(visitor, ClassReader.EXPAND_FRAMES);

可以看出来,ClassProbesAdapter应该是ASM框架中的适配器(即继承自ClassVisitor,自定义对字节码文件过滤的类),同时在ClassInstrumenter中,发现其visitMethod()函数返回了MethodInstrumenter对象,在该类中,找到了具体的插桩方法。
首先在MethodProbesAdapter中,定义了插桩策略

public void visitInsn(final int opcode) {
        switch (opcode) {
        case Opcodes.IRETURN:
        case Opcodes.LRETURN:
        case Opcodes.FRETURN:
        case Opcodes.DRETURN:
        case Opcodes.ARETURN:
        case Opcodes.RETURN:
        case Opcodes.ATHROW:
            probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
            break;
        default:
            probesVisitor.visitInsn(opcode);
            break;
        }
    }

然后在MethodInstrumenter中具体实现了各个策略。示例:

public void visitJumpInsnWithProbe(final int opcode, final Label label,
            final int probeId, final IFrame frame) {
        if (opcode == Opcodes.GOTO) {
            probeInserter.insertProbe(probeId);
            mv.visitJumpInsn(Opcodes.GOTO, label);
        } else {
            final Label intermediate = new Label();
            mv.visitJumpInsn(getInverted(opcode), intermediate);
            probeInserter.insertProbe(probeId);
            mv.visitJumpInsn(Opcodes.GOTO, label);
            mv.visitLabel(intermediate);
            frame.accept(mv);
        }
    }

具体插入是probeInserter.insertProbe(probeId);,它在ProbeInster中被实现

生成报告

生成报告在程序入口在ReportTask中,传入了executionData(多个executionData文件可以合并),sourcefiles和classfiles。其中executionData是运行时被插桩的字节码探针执行情况。

  • classfiles:它指定了Java class文件的文件夹。
  • sourcefiles:可选的容器元素,指定了相关的源文件。如果源代码被指定,报告将会包含高亮代码。源文件可以被指定为独立文件或者文件目录

根据运行时数据,运行时数据就是classname,id,以及16进制的表示的指针数组文件,具体使用应该会转换成二进制,表示覆盖率执行的结果,加class文件路径解析出类覆盖,方法覆盖等结果,再结合源代码路径生成高亮,可视化强的代码报告,就是我们上图看到html测试覆盖率文件

编译调试信息

java编译成class可以生成调试信息,这样就可以从类文件中拿到对应class的信息,计算行的覆盖率并让源码高亮显示

-g:source Class文件中固定长度的SourceFile属性,编译时如果选择了生成此属性,则会将它写到Class文件中。它用来提供产生Class文件的源文件名称

-g:lines 将源文件中的行号信息写到Class文件中,此属性用于在Class文件中生成方法字节码流偏移量和源代码行号之间的映射关系。就像我们引子中所看到的,打印出的异常栈中看不到行号,就是因为生成Class中没有此属性导致,而远程调试时打不上断点也是这个原因

-g:vars属性建立了方法的栈帧中局部变量部分内容与源代码中局部变量名称和描述符之间的映射关系。简单一点说,如果没有此属性的信息,那我们在调试时将无法看到变量值

接入过程的问题

diff-cover的报错

No lines with coverage information in this diff
一般出现该问题,主要是路径原因,可尝试使用绝对路径,与jacoco搭配使用,需要使用--src-roots命令引入源代码

diff-cover的环境

diff-cover目前只支持mac环境

混淆问题

jacoco不能混淆,jenkins打的release包,反射调用代码找不到
需要配置

        -keep class org.jacoco.** { *; }
        -keep interface org.jacoco.** { *; }

支持kotlin

1.增加java源码路径

def coverageSourceDirs = [
        '../xxx/src',
        '../xxx/kotlin'
]

2.编译后class插桩路径

 def classTree = fileTree( dir: 'xxxxxx')
 def kotlinTree = fileTree(dir: './build/tmp/kotlin-classes/debug')
 classDirectories = files([classTree], [kotlinTree])

release与debug

本质上我们只需要打debug包就行,release不需要配置,但是由于某种原因jenkins平台打的是release包,为了不影响原有的工作流程,所以release也支持了测试覆盖率
但是在执行脚本的过程中,release包需要classDirectories为build下release的路径
debug包需要classDirectories为build下debug的路径
即使jenkin打包本地生成,也需要走release的路径,本地需要打release包,构造出相应的build路径,没有签名怎么办?使用debug签名即可

参考

diff-cover https://github.com/Bachmann1234/diff_cover
jacoco接入 https://github.com/Jay-Goo/JacocoTestHelper
https://testerhome.com/topics/16376
https://tech.meituan.com/2017/06/16/android-jacoco-practace.html
https://www.eclemma.org/jacoco/trunk/doc/counters.html
https://mp.weixin.qq.com/s?__biz=MzIxNzEyMzIzOA==&mid=2652314564&idx=1&sn=a93e6154c92acaef9204b8440e66a852&scene=21#wechat_redirect
https://blog.csdn.net/ohcezzz/article/details/78416125
https://www.jianshu.com/p/639e51c76544
https://www.iteye.com/blog/daimojingdeyu-679030

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容