乐趣区

关于软件测试:干货-精准化测试原理简介与实践探索

↑点击上方卡片关注我↑

小时候大家应该都玩过一个游戏,游戏很简略,就是找不同,在规定工夫内两幅图间接的差别点找到就算赢,越快越好,就像上面这样:

下面这个不同点想找很简略,那么上面这样的呢?

这个,的确有的人会说 ” 我能够!”。比方在综艺节目 ” 最强大脑 ” 中,这群 ” 变态 ” 的非人类的确能够

反正我不行,我也不信你们看到文章这里的人能够~ 我只有最菜大脑

实践上,咱们全面的测试笼罩,必定就就能够保障,那么咱们先看下上面的代码:

这是一份波及订单状态的各种枚举,每一个状态的背地都有其业务逻辑,甚至还有穿插,假若依照笛卡尔积或者正交的形式来进行用例设计与笼罩,有。。。好多好多用例

  • 那么~ 你真的有那么多工夫去全笼罩吗?
    开发:我改了点代码,等会帮忙全面回归一遍吧
    测试:好的 ( bi~~ )
    什么?自动化?Are you sure?

    测试倒退到现在,如同不会点自动化,都不好意思叫测试,简历上不写点自动化都拿不出手,然而自动化真的是测试的银弹不,做过的应该深有感触,自动化属于一个奢侈品:

  • 开发副本
  • 保护老本
  • 如何应用
  • 用例的设计合理性
  • 新性能的滞后性

再者,你确定你真的笼罩到了被测代码?也就是相当于魔方墙上的每个色块,理论在黑盒测试的过程中很大水平上取决于测试人员的教训,主观性很强,这样就很可能漏测,公布后出了问题就又要开撕了。。。

可能有的小伙伴会这样感觉,有人通知咱们答案,也就是通知咱们魔方墙的差别之处。这样我不就晓得关注的测试点了吗?

没错,咱们能够让开发通知咱们本次改了哪些办法,甚至有代码权限的状况下咱们有能力能够本人去剖析代码,妥了,金女士!

那么问题又来了。针对下面的状况,开发的形容肯定是正确全面的吗?即便开发精确的阐明了改变的代码,那么改变所影响到的其余范畴呢?开发自己也不好确认的(不然还要测试干啥~),开发也有可能偷偷改代码不通知你呢。

这个时候就渴望有这么一个 ” 最强大脑 ”

  • 眼过来就能够看出差别点(本次改变的逻辑)
  • 脑海中就有了差别的影响范畴(放大须要测试的范畴)
  • 再一扫就看出哪些测试笼罩到了(确认测试覆盖率)

以求达到一种精准测试的水平

依照下面的形容,大略咱们能够分为三个维度:

  • 差异化
  • 调用链
  • 覆盖率
    接下来的文章中会一个个具体来说~

不同的语言,都会有对应不同的语法分析器,语法分析器会把源代码作为字符串读入、解析,并建设语法树,这是一个程序实现编译所必要的前期工作。

咱们看下 Java 的编译过程,重点关注步骤一和步骤二:

这里咱们应用一个简略的 Java 对象,解析成 AST 后看下长什么样子

因为层级太多太简单,这里选取属性 user 做个简略演示阐明。如下:

每一项外面都蕴含了最全面的信息,包含名称、行号等,具体的能够拜访在线调试网站 https://astexplorer.net/ 进行调试查看

既然所有的代码信息都有了,那么咱们就能够拿着这些信息进行比对,从而找出代码的差别之处;(当然这其中还是要很多降噪解决的,例如正文、空格、业务无关代码 get/set 等)
大略的流程逻辑如下

3.2.1 字节码

因为 Java 代码的运行,是通过 javac 先将 Java 文件编译成.class 结尾的字节码,再由 JVM 去执行;所以在字节码文件中,领有了足够的元数据来解析类中的所有元素:类名称、父类名、办法、属性以及 Java 字节码(指令);

以如下源码为例:

1  public class AccurateTest {
2
3     private int a = 1;
4
5     public String add(int b){6        return String.valueOf(a + b);
7    }
8 }
9

命令将其编译为字节码文件,再应用
命令将其反编译后失去如下信息:

Classfile /Users/qinzhen/Documents/My/TrainingProject/calctest/src/test/java/AccurateTest.class
  Last modified 2021-7-15; size 386 bytes
  MD5 checksum e67842e9b540c556d288c28b303298fb
  Compiled from "AccurateTest.java"
public class AccurateTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#20         // AccurateTest.a:I
   #3 = Class              #21            // AccurateTest
   #4 = Class              #22            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LAccurateTest;
  #14 = Utf8               add
  #15 = Utf8               (I)I
  #16 = Utf8               b
  #17 = Utf8               SourceFile
  #18 = Utf8               AccurateTest.java
  #19 = NameAndType        #7:#8          // "<init>":()V
  #20 = NameAndType        #5:#6          // a:I
  #21 = Utf8               AccurateTest
  #22 = Utf8               java/lang/Object
{public AccurateTest();            // 构造函数
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field a:I
         9: return
      LineNumberTable:
        line 1: 0
        line 3: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   LAccurateTest;

  public java.lang.String add(int);        // 办法名
    descriptor: (I)Ljava/lang/String;      // 办法描述符(入参和返回值类型)                
    flags: ACC_PUBLIC              // 办法的拜访标致
    Code:                    //code 开始
      stack=2, locals=2, args_size=2
         0: aload_0
         1: getfield      #2                    // 援用常量池的值 Field a:I
         4: iload_1
         5: iadd
         6: invokestatic  #3                    // Method java/lang/String.valueOf:(I)Ljava/lang/String;
         9: ireturn
      LineNumberTable:              // 行号表,将上述操作码与.java 中的行号做对应
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   LAccurateTest;
            0       7     1     b   I      // 本地变量
}
SourceFile: "AccurateTest.java"

通过上述信息咱们能够直观的看到字节码中蕴含了 Java 运行所需的所有信息,且 JVM 对于字节码文件要求严格,必须依照固定的组成和程序,而这种个性也就适宜利用访问者模式对字节码文件进行批改;因而也就要介绍咱们的调用链生成的核心技术栈——ASM

3.2.2 ASM

操作;
API 接口,每当
, 扫描到类注解就会回调
等;
办法来实现字节码的读取和插入,例如在做调用链分析时咱们就用到了其
办法来对办法体内的调用信息进行过滤和提取

通过上述的信息进行匹配桥接,咱们就能够拿到调用链中的一系列父子节点,造成咱们的办法调用链

大略的流程逻辑如下:

说到覆盖率统计,就要介绍以后在这个技术畛域中占据主导地位的开源工具 -jacoco
jacoco 应用总的来说和装大象一样,须要三步

    1. 对被测我的项目进行字节码插桩
    1. 覆盖率数据的采集与导出
    1. 覆盖率数据的统计与报告生成
      上面咱们对这三个步骤逐个拆解
      插桩,其实就是安插监控探头,咱们的一行行代码就好比一条条马路,代码里的分支 (if-else) 就好比马路上的各种支路岔道,而插桩就相当于在每一条路的路口都装上了一个探头

如下就是在字节码中插入探针信息的图示:

jacoco 的插桩模式有两种:

  • on-the-fly 模式(运行时插桩)
  • 通过配置 -javaagent 在启动命令中,jacoco 染指被测我的项目部署过程,将探针 (探头) 插入 class 文件,探针不扭转原有办法的行为,只是记录是否曾经执行。
  • 长处:无需提前进行字节码插桩,无需思考 classpath 的设置。
  • 毛病:要批改 JVM 参数,对环境的要求比拟高,于一些无奈批改启动命令的场景不实用。
  • offline 模式(编译时插桩)
  • 在测试之前先对文件进行插桩,生成插过桩的 class 或 jar 包,测试插过桩的 class 和 jar 包,生成覆盖率信息到文件,最初对立解决,生成报告。
  • 长处:屏蔽工具对虚拟机环境的依赖;
  • 毛病:须要提前侵入代码;无奈实时获取覆盖率,只能测试实现后进行我的项目后对立生成报告
    抉择:

形式毋庸入侵利用启动脚本,再加上公司的运维和开发能够配合部署
启动参数,因而咱们最终抉择
模式进行插桩

3.3.2 覆盖率收集与导出
看了下面的插桩原理,想必覆盖率的收集也就很好了解了,仍然是以监控探头为例,当咱们测试一行行代码时,就相当于开着车跑在一条条路线上,而每进入一行代码就像是开车进入了一条路线,那么进入的时候就会被监控探头拍摄记录下来,也就晓得你跑过哪条路了。
同理,笼罩到一行代码时,探针就会记录下信息,最终也就晓得了哪一行代码被笼罩到了

至于导出,覆盖率的统计信息会通过裸露的服务端口 (默认 6300) 去获取,导出一份以.exec 结尾的文件,文件中蕴含了以后的覆盖率信息

通过对 exec 文件的解析,jacoco 便能够获取所有办法的探针信息,从而计算覆盖率,并对代码进行染色输入报告:

针对代码的染色如下

  • 红色:代表未笼罩
  • 黄色:代表局部笼罩,
  • 绿色:代表齐全笼罩
    在理论的应用场景中,咱们可能还更关注本次批改的代码,测试的时候咱们会重点测试本轮开发的新增和改变范畴,因而 jacoco 原生的性能就不能满足了,jacoco 原生统计的是全量的覆盖率。

对于改变点,咱们称之为增量,所以咱们对 jacoco 的源码进行了二次开发,使其反对增量的覆盖率统计,以满足日常测试需要;比照下面全量的范畴,能够看到增量的统计范畴就明确了,数量就少了很多:

  • 大略的架构逻辑如下:

开发批改了一个办法或者一个接口,那么这个接口可能被 N 个利用去调用,一旦这个接口有问题,那么影响面是相当大的;或者这个接口自身没问题,然而上下游没有兼容好,调用出了问题也是影响产品质量的;所以这个也是咱们测试关注的重点。
再者,咱们日常的测试有很大一部分比例是接口测试,包含自动化也是,接口自动化用例很多。那么如果能够通过调用链路找到本次批改所影响到的最上层的入口接口 (
等),那么通过接口与用例的关联关系,就能够举荐出本轮批改必须要执行的用例,进步用例的精准水平和更加明确的测试范畴。
还有,如果改变的接口没有关联的用例,或者用例执行完当前覆盖率不达标,那么也能够对用例进行查漏,增加新的用例进行笼罩。

  • 长处:计划绝对成熟,业界有落地案例,实现难度尚可
  • 毛病:链路也是通过插桩监控的,那么前提就是这条链路要走到了才会存在,这样就有滞后性,新减少的代码链路还没有测试过,那这条链路天然也就拿不到了

聊到这里,基本上就把测试人员的灵魂 3 问给答复结束了。对于精准化测试,这里有几个问题会困扰测试开发人员。这里给出一些倡议,心愿能够对读者有所好处。
1、如果我的代码覆盖率达到 100% 了,是不是就能够说测试笼罩齐全了,品质有保障了?

答:不是,覆盖率低,品质肯定没有保障,然而覆盖率高,只是保障的一个维度达到了。
这里咱们只是晓得了代码被笼罩了,然而代码逻辑的正确性呢?精准化是无奈判断的,要靠大家本人去断言了。
再者,笼罩到的代码都是开发依照本人了解的业务逻辑写的,如果他漏写了一些需要逻辑呢?那这部分就不存在笼罩的状况了。

2、我是不是每次都要保障所有的办法覆盖率都达到 100%?

答:不是,办法的覆盖率要达到什么样的一个值,不好间接下结论。有些代码逻辑,好比一些异样的捕捉,这个异样的触发场景很难,日常测试简直走不到,那么就是笼罩不了,覆盖率也就不可能达到 100%。

3、依据问题 2,既然达不到 100%,那么我是不是设一个阈值,好比 80%?90%?,达到这个阈值就能够了?

答:也不是,有些办法,它的代码逻辑可能都是外围逻辑,其中的分支都须要笼罩,短少了就有漏测出 Bug 的危险,且实践上都是能够通过测试笼罩到的,那么这种办法就须要达到 100% 的覆盖率。

4、那要怎么掂量覆盖率的指标?

答:一方面能够设定一个最低阈值,哪怕代码有些逻辑走不到,也不会大面积并且占比很高,还是须要一个最低的覆盖率保障;
再者,须要测试的同学依据本人测试的业务进行状况划分,具备 codereview 的能力和习惯,平台仅作为一个辅助测试的工具;
最初,咱们能够记录下以往测试的覆盖率,依据不同业务通过测试后的覆盖率状况统计覆盖率的趋势,以历史的覆盖率数据为根据来设定阈值或监控告警,如果覆盖率低于往期失常的值,就进行告警或者卡点

退出移动版