干货 | 精准化测试原理简介与实践探索

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 干货 | 精准化测试原理简介与实践探索

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

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

这个,确实有的人会说"我可以!" 。比如在综艺节目"最强大脑"中,这群"变态"的非人类确实可以

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

理论上,我们全面的测试覆盖,肯定就就可以保证,那么我们先看下下面的代码:

这是一份涉及订单状态的各种枚举,每一个状态的背后都有其业务逻辑,甚至还有交叉,假若按照笛卡尔积或者正交的方式来进行用例设计与覆盖,有。。。好多好多用例

1080×324 230 KB


898×180 115 KB

  • 那么~你真的有那么多时间去全覆盖吗?
    开发:我改了点代码,等会帮忙全面回归一遍吧
    测试:好的(*** 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. 对被测项目进行字节码插桩
  2. 覆盖率数据的采集与导出
  3. 覆盖率数据的统计与报告生成
    下面我们对这三个步骤逐一拆解
    插桩,其实就是安插监控探头,我们的一行行代码就好比一条条马路,代码里的分支(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的能力和习惯,平台仅作为一个辅助测试的工具;

最后,我们可以记录下以往测试的覆盖率,根据不同业务通过测试后的覆盖率情况统计覆盖率的趋势,以历史的覆盖率数据为依据来设定阈值或监控告警,如果覆盖率低于往期正常的值,就进行告警或者卡点

相关文章
|
5天前
|
敏捷开发 人工智能 Devops
探索自动化测试的高效策略与实践###
当今软件开发生命周期中,自动化测试已成为提升效率、保障质量的关键工具。本文深入剖析了自动化测试的核心价值,探讨了一系列高效策略,包括选择合适的自动化框架、设计可维护的测试脚本、集成持续集成/持续部署(CI/CD)流程,以及有效管理和维护测试用例库。通过具体案例分析,揭示了这些策略在实际应用中的成效,为软件测试人员提供了宝贵的经验分享和实践指导。 ###
|
4天前
|
机器学习/深度学习 人工智能 jenkins
软件测试中的自动化与持续集成实践
在快速迭代的软件开发过程中,自动化测试和持续集成(CI)是确保代码质量和加速产品上市的关键。本文探讨了自动化测试的重要性、常见的自动化测试工具以及如何将自动化测试整合到持续集成流程中,以提高软件测试的效率和可靠性。通过案例分析,展示了自动化测试和持续集成在实际项目中的应用效果,并提供了实施建议。
|
5天前
|
Java 测试技术 持续交付
探索自动化测试在软件开发中的关键作用与实践
在现代软件开发流程中,自动化测试已成为提升产品质量、加速交付速度的不可或缺的一环。本文深入探讨了自动化测试的重要性,分析了其在不同阶段的应用价值,并结合实际案例阐述了如何有效实施自动化测试策略,以期为读者提供一套可操作的实践指南。
|
5天前
|
Web App开发 敏捷开发 测试技术
探索自动化测试的奥秘:从理论到实践
【10月更文挑战第39天】在软件质量保障的战场上,自动化测试是提升效率和准确性的利器。本文将深入浅出地介绍自动化测试的基本概念、必要性以及如何实施自动化测试。我们将通过一个实际案例,展示如何利用流行的自动化测试工具Selenium进行网页测试,并分享一些实用的技巧和最佳实践。无论你是新手还是有经验的测试工程师,这篇文章都将为你提供宝贵的知识,帮助你在自动化测试的道路上更进一步。
|
5天前
|
敏捷开发 Java 测试技术
探索自动化测试:从理论到实践
【10月更文挑战第39天】在软件开发的海洋中,自动化测试是一艘能够带领团队高效航行的船只。本文将作为你的航海图,指引你理解自动化测试的核心概念,并分享一段实际的代码旅程,让你领略自动化测试的魅力和力量。准备好了吗?让我们启航!
|
10天前
|
测试技术 API Android开发
探索软件测试中的自动化框架选择与实践####
本文深入探讨了软件测试领域内,面对众多自动化测试框架时,如何依据项目特性和团队需求做出明智选择,并分享了实践中的有效策略与技巧。不同于传统摘要的概述方式,本文将直接以一段实践指南的形式,简述在选择自动化测试框架时应考虑的核心要素及推荐路径,旨在为读者提供即时可用的参考。 ####
|
21天前
|
机器学习/深度学习 人工智能 自然语言处理
探索软件测试的边界:从基础到高级的实践之旅
【10月更文挑战第21天】 在当今数字化时代,软件已成为我们生活和工作中不可或缺的一部分。随着技术的快速发展,对软件质量的要求也日益提高。本文旨在通过深入浅出的方式,带领读者踏上一场从基础到高级的软件测试实践之旅。我们将探讨软件测试的基本概念、重要性以及如何有效地进行测试规划和执行。通过具体案例分析,揭示常见错误及其解决方案,同时展望未来软件测试领域的发展趋势。无论你是软件开发新手还是经验丰富的测试工程师,这篇文章都将为你提供宝贵的见解和启发。
36 8
|
20天前
|
监控 安全 jenkins
探索软件测试的奥秘:自动化测试框架的搭建与实践
【10月更文挑战第24天】在软件开发的海洋里,测试是确保航行安全的灯塔。本文将带领读者揭开软件测试的神秘面纱,深入探讨如何从零开始搭建一个自动化测试框架,并配以代码示例。我们将一起航行在自动化测试的浪潮之上,体验从理论到实践的转变,最终达到提高测试效率和质量的彼岸。
|
15天前
|
NoSQL 测试技术 Go
自动化测试在 Go 开源库中的应用与实践
本文介绍了 Go 语言的自动化测试及其在 `go mongox` 库中的实践。Go 语言通过 `testing` 库和 `go test` 命令提供了简洁高效的测试框架,支持单元测试、集成测试和基准测试。`go mongox` 库通过单元测试和集成测试确保与 MongoDB 交互的正确性和稳定性,使用 Docker Compose 快速搭建测试环境。文章还探讨了表驱动测试、覆盖率检查和 Mock 工具的使用,强调了自动化测试在开源库中的重要性。
|
20天前
|
测试技术 Python
软件测试的艺术:从理论到实践的探索
【10月更文挑战第23天】在软件开发的世界中,测试是确保产品质量的关键步骤。本文将深入探讨软件测试的核心理念、方法和实践,揭示如何通过精心设计和执行测试来提高软件质量。我们将一起探索测试的不同阶段,包括单元测试、集成测试、系统测试和验收测试,以及它们如何相互补充,共同构建起一个坚实的质量保证体系。文章旨在启发读者思考如何在自己的项目中实施有效的测试策略,从而提升软件的可靠性和性能。