测试之道测试发展之路

Posted sysu_lluozh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了测试之道测试发展之路相关的知识,希望对你有一定的参考价值。

一、测试团队面临的困局

随着互联网的发展,持续集成及DevOps等软件工程方法的提出和普及,软件测试技术也发生了很大的变化。自动化测试、敏捷测试和测试左移等方法论被广泛应用于实践,研发团队对测试技术和测试团队有更高的期待。与此同时,测试团队也面临着不少的困难,其中具有普遍性的有:

  • 测试自动化难

自动化测试程序编写成本高、执行成本高、维护成本高。测试的技术债在业务高速发展的过程中越积越多:

  1. 一边修复老的用例,另一边出现新的失败用例
  2. 一边补充自动化,另一边又因为项目的时间压力遗留了未自动化的用例

一些可以自动化完成的功能,回归测试还得依赖手动测试,效率低、问题遗漏多

  • 测试结果的噪声大

回归测试的通过率比较低,每次回归测试都需要排查大量的失败用例,大部分的失败都是由于测试环境及相互干扰,而不是被测服务的缺陷引起,这样导致测试人员很难获得成就感
另外,测试结果的噪声多次导致回归测试已经发现的问题在排查中遗漏,变成线上问题

  • 测试不充分

测试分析和用例枚举非常依赖测试人员的经验和业务领域知识,测试人员很容易出现测试遗漏。同时,缺少有效的技术手段度量和提升测试的充分性

  • 测试人员对自身成长的焦虑

测试团队的困局导致测试人员需要把大量的时间花在各种重复工作和琐事上,没有时间和精力提升自身能力、追求技术创新,也缺少沉淀积累

二、理解测试的本质

2.1 代码门禁

开发人员一般都是通过git push origin命令将代码直接提交到远程的项目分支和迭代分支。虽然一直明确要求开发人员必须确保代码在本地编译及通过所有的单元测试和接口级别的功能测试后才能提交,但偶尔有些开发不遵守这个规定。而且由于环境的差异,有些代码能在本地通过测试,但是在其他地方运行还是会失败,代码门禁可以彻底杜绝这些问题
代码门禁的做法如下:

  • 在公共分支(包括项目分支、迭代分支、主干分支、紧急发布分支)上,禁用git push,只允许通过pull request来提交代码
  • 研发平台对每个提交到公共分支的pull request都会执行各项检验,包括编译、单元测试和接口级别功能测试、静态代码扫描。只有通过这些检验,pull request才能够被合并

代码门禁就是简单地把持续集成前移:从代码提交后提前到代码提交前
简单的前移,彻底改变了研发模式,从以前的"先欠债再还"变成"每个代码提交都不能欠债"

门禁虽然很重要,但是还必须做一些关键的优化才能有效的推广:

1. 缩短时间

  • 制定代码门禁的测试执行时间的目标。要求每个微服务代码门禁执行时间都不超过10分钟
  • 基于MariaDB4j的本地数据库方案,解决了被测代码对远程数据库的依赖问题
  • 精准测试,只运行和变更代码相关的用例。记录每个用例的代码覆盖的详细数据,当代码门禁测试开始时,基于这些数据反查出和变更代码相关的用例

2. 提高稳定性

  • 制定代码门禁的测试稳定性的目标,即90%的成功率。也就是说整个测试用例集如果执行100次,其中至少有90次是通过的
  • 本地数据库方案和精准测试提高稳定性。使用精准测试后执行的用例数量降低,出现噪声的可能性也降低

2.2 测试的本质是反馈

代码门禁是测试团队打破困局的第一步,但远远不是全部。在进一步阐述破局的思路前,有必要先弄清楚测试的本质

测试的本质是反馈,就是回答一个问题:代码是不是好的
这里的好的定义并不是一成不变的,对于不同的需求,好的定义和标准都是不同的。换句话说,测试的目的是提供质量反馈,为整个软件开发过程中不同的节点提供以下3个用于决策的质量反馈

  • 代码门禁中的单元测试和接口测试接口,为判断是否可以接受代码提交提供决策依据
  • 功能回归的测试结果,为判断是否可以将当前版本的代码推进到预发布和灰发验证阶段提供决策依据
  • 预发布环境和灰度验证的结果,为判断是否可以将当前版本的代码进一步部署到整个线上环境提供决策依据

只要测试是足够全面、结果是足够可信,且所有测试都通过,就可以直接做出决策。希望减少对人的经验、知识和判断的依赖。一个组织一定会不断的新陈代谢,一个人能了解整个系统的各种细节和一块业务的方方面面的可能性越来越低

测试团队是从缩短提供反馈的时间、降低提供反馈的成本和提高反馈的可信度三个维度来提升测试反馈能力,这三个维度既是质量视角,也是效能视角

2.3 效能提升对质量的作用

有人认为质量提升和效能是一对矛盾体,要提升效能就要牺牲质量,要提高质量就要牺牲效能。其实质量和效能并不是一对矛盾体,而是既要…也要…的关系。效能提升可以让我们更快地得到反馈,以更快的节奏去迭代和试错

2.3.1 效能提升对提高质量的直接作用

  • 效能提升能够让代码中的质量问题更及时地暴露出来
    例如用例执行到BUG A抛错,直到BUG A修复,用例才能继续执行,之后遇到BUG B,其实BUG B早就存在,只是一直被BUG A掩盖

  • 效能提升能够让代码的质量问题更容易地暴露出来
    可以让质量问题暴露,而不是淹没在各种噪声中。例如接口测试或者全链路回归的确出现了问题,但是由于信噪比长期很低,有效信号很容易被开发人员和测试人员忽视

  • 更完善的工具和基建,能够直接减少人为错误的发生

2.3.2 效能提升对提高质量的间接作用

  • 降低心力、脑力的负担
    在项目周期长、多个项目并发的情况下,测试工程师每天都要在各个项目之间频繁进行上下文切换,对心力和脑力都是极大的负担,不免挂一漏万,增加了漏测和未能识别的潜在风险存在 的可能性

  • 提升测试开发人员的价值
    工具和基础建设做的更好,可以把花在解决比较低级问题上的时间节省下来,用在对测试和质量有更大价值的地方。反过来,质量的提高可以让我们更有信心用更少的资源、更短的时间做出决策和判断

2.4 破局的方向

在对测试困局进行破局的过程中,以及未来的道路上,做的工作基本上都可以映射到反馈的时间、成本和可信度3个维度上

2.4.1 缩短反馈弧(时间)

  • 测试用例向测试金字塔的下层迁移
  • 提高测试并发执行的能力
  • 减少数据准备的时间,既要能够更好的复用测试数据,又要避免因测试数据复用导致的测试不稳定
  • 打造精准测试能力,能够自动准确地剔除无关的用例
  • 能够通过对测试用例有效性、代码覆盖率和业务覆盖率的准确度量,帮助更好地进行等价类分析,对测试用例集进行持续瘦身,避免用例集不断膨胀

2.4.2 降低反馈成本

测试用例向金字塔的下层迁移,将更多的测试用更快、更省资源的小型测试实现,精简测试用例、只运行相关的用例,通过缩短测试执行时间来减少对资源的占用
从硬隔离向软隔离转变,用更多的逻辑隔离替代实例隔离

2.4.3 提高反馈可信度

  • 提高测试的可信度,降低测试结果的噪声
  • 提高测试的代码覆盖率和业务覆盖率
  • 提高测试的有效性
  • 自动生成测试用例
  • 减少人工进行测试分析和罗列测试用例产生的测试遗漏

在以上3点中,缩短反馈弧是关键

三、测试的提升

3.1 缩短反馈弧

3.1.1 为什么缩短反馈弧是关键

没有度量就没有改进,其实这句话是不完整的。度量对于改进的作用是给出反馈,但是光有度量还不够,还要度量得足够频繁、度量得足够快,这样才能更有效地改进
工作中有很多例子:

  • 线上变更强调可监控
    做了一个变更,如果能马上得到高质量的反馈(高质量的反馈=监控覆盖率高+噪声低+阈值设定得合理),就非常有助于判断这个变更是好的还是不好的。因此,缩短反馈弧非常有助于及时发现问题、及时采取对策

  • 业务试错也是反馈
    试错就是试一下,看看错不错,如果错了就掉头,如果对了就继续投入。快速试错要求缩短业务效果的反馈弧。没有试错能力,就是反馈弧长

3.1.2 怎么才算反馈弧短

反馈弧短不短,可以从两个方面衡量

  • 反馈的前置等待时间
    理想状态是:反馈不需要等,任何时候想要反馈都可以
  • 反馈本身的耗时
    理想状态是:反馈本身的耗时很短,结果立等可取

一名开发人员,写好一行代码,想知道这行代码有没有问题,这就是反馈
把某个功能需要的代码写好,现在想知道这个功能是不是能工作,代码还要不要改,这也是反馈

在有些团队里,这些反馈弧还很长,长在两个方面:
一个是要等,不是任何时候想要得到反馈都可以
一个是反馈本身耗时长、成本高,结果也不是立等可取的

反馈弧一长,开发效率就降低。在一些团队中,反馈弧在开发联调中体现为:

  • 反馈不是随时随地的,要等
    因为反馈不是随时随地都可以发起的,也不是每个人都知道怎么发起的,只有特定的人员才知道怎么发起

  • 反馈不是立等可取的
    就算发起了反馈,还要找一个个域的人员校验数据。同时,反馈的质量因人而异,因为校验是人做的,不同人校验的方法是不完全一样的

平时一直做各种动作,比如改代码,给数据库加字段、修改DRM值、在数据库中插入数据等。持续集成就是在这些动作发生之后,尽可能快的给出反馈,缩短反馈弧
持续指反馈要随时随地都可以得到,自动化是缩短反馈弧的必要条件(但不是充分条件,此外还包括覆盖率、充分性、有效性等要素)
如果还有人工步骤,就不可能做到反馈弧很短,因为人是不可能随时随地都能一呼即应,而且人的动作也很难控制的

3.1.3 缩短反馈弧的成本和投入产出比

要缩短反馈弧,就要建设持续集成
虽然持续集成的确需要投入成本,但是不仅要看到建设持续集成的投入,也要看到回报。这个做好,能节省多少时间,在缩短反馈弧上投入的成本是能从其他地方收回来的

对于每个项目来说,如果每个联调用例和校验都有自动化的投入,在项目进行的过程中就会有回报。比如,人工做一次校验可能需要15分钟,而校验自动化需要2小时(包括自动化及后期的维护成本),只要整个项目过程中校验超过8次,自动化就是划算的,而且如果实现自动化,校验就会执行很多次,因为希望可以随时随地得到反馈
另外,随时随地做校验、随时随地地得到反馈,还有一个好处,就是能让排查问题变得很方便

不能孤立地看待投入在建设持续集成上的成本。如果只从单个开发人员的视角看,那么也许在某些情况下,用原来的方式对个人更方便。但在更多情况下,个体收益往往导致群体受损,进而导致每个个体都受损。相反,个体做出一些小的付出,会导致整个群体受益,也就是让每个个体都受益

3.2 提升测试的稳定性

很多测试团队面临的最大难题就是自动化测试的通过率低、噪声大。在理想情况下,希望每个失败的测试用例都是由真正的缺陷引起的。但在实际情况是,自动化测试经常因为其他原因导致失败,例如:

  • 同时执行的用例之间相互影响
  • 人对测试环境的影响
  • 测试环境底层基建的不稳定
  • 测试环境里的脏数据
  • 测试用例本身有问题

自动化测试稳定性差带来的后果:

  • 失败用例的排查工作量非常大

  • 对自动化测试丧失信任和信心

  • 如果发现大部分失败用例都是由于非代码缺陷原因,有些人员就不会重视,这样很多真正的缺陷就会被遗漏

  • 更多的问题被掩盖

接下来重点讲述如何达到和保持很高的测试稳定性,提升测试稳定性有三斧子半的招式:第一招 ---- 高频,第二招 ---- 隔离,第三招 ---- 用完即抛,最后半招 ---- 不自动重跑

3.2.1 高频

高频之所以排在第一位,是因为它不但对提高测试稳定性有奇效,也有很多其他软件工程中解决问题的方法

举几个通过高频解决问题的例子:

  • 持续打包
    只在部署测试环境前打包,那么经常会因为打包问题导致部署花费很多时间,影响后面的测试进度。针对这个问题,可以持续打包。每隔半个小时或者一个小时对master或者项目分支的HEAD打包,一旦遇到问题,马上修复

  • 发布
    如果在发布过程中问题很多,可以尝试更高频地发布。如果原来每周发布一次,就改成每天发布一次。除了原来每周一次的发布,一周里的其余日子就算没有新代码合并进来,也要空转发布一次。这样做的目的是用高频来暴露问题,倒逼发布过程中各环节的优化

  • 证书和密钥的更新
    证书容易过期?证书更新的过程不够自动化?使用高频的方式。把原来两年一次甚至更久一次的证书更新频率提高到每三个月或者六个月一次。用这样高频率来倒逼证书更新的自动化,暴露证书管理机制中的问题

  • 容灾演练
    为了加强容灾能力建设、提高容灾演练的成功率,一个主导思想就是高频演练,用高频演练来充分暴露问题,倒逼能力建设

另外,从测试稳定性上看,高频测试的好处有以下几点:

  • 缩短反馈弧
    如果一个团队进行功能回归测试的频率是一天一次,那么星期一做的代码改动需要星期二才能看到。更糟糕的是星期二测试执行结果显示星期一的代码有问题,那么即便星期二当天解决问题时也需要等到星期三才能看到修复效果。这样以天计的节奏显然不符合敏捷开发和业务快速发展的要求。期望的节奏是:早上10点修改的代码,午饭前就可以看到功能测试结果,根据测试结果,做相应的改动时,下午就能通过功能测试回归

  • 变主动验证为"消极等待",减少测试人员的工作量
    开发人员修复一个失败用例背后的代码问题,测试人员要手动出发这个用例检查是否通过。当缺陷数量较多时,测试人员工作量比较大。有了高频的功能回归后,功能回归用例集每小时都执行一遍最新代码。当开发人员提交了缺陷修复后,测试人员不再需要手动出发用例,只需要消极等待下一个小时的测试结果

  • 识别和确认小概率问题
    当一天只有一次测试时,如果开发人员说一个用例失败的原因是数据库抖动,那么也许能接受。但当每小时都有一次测试,连续三次都得出同样的失败结果时,数据库抖动的理由就说不通了,可以推动开发人员进行更深入的排查

  • 暴露基建层的不稳定因素

  • 倒逼人工环节自动化
    如果一个团队进行功能回归的频率时一天一次,那么其中一些人工步骤可能会被容忍。例如,敲一行命令、编辑一个配置文件、设置几个参数、点几下按钮。这些手动步骤本身可能出错,导致测试通过率下降。而用高频的手段比如把功能回归的频率从一天一次提高到一小时一次,那么就无法容忍这些步骤,就有动力把这些步骤改成自动化实现,从而也减少人工出错的可能

  • 为分析提供更多的数据

针对高频问题的重要性的一些疑问:

  • 问题一:为什么一定要定时进行测试?为什么不能变成每次有代码提交再触发?
    可以让每次代码提交都触发测试执行,但定时的测试仍然必须要有,因为代码提交的频率是不均匀的。在每天代码提交较少的时间段,即便代码没有变,仍然希望保持测试执行的频率,以便及时发现基建的问题。另外,需要保持测试执行的频率,以产生足够的数据来分析、识别和确认小概率问题

  • 问题二:进行高频测试,需要更多的资源,成本很高,值得吗?
    高频测试对测试资源的需求的确会成倍地增长,甚至是呈数量级的增长。但是花这么多成本来做高频测试,换来的是自动化的噪声大大降低、无效排查大大减少,被掩盖的质量问题大大减少,是值得的。另外,高频测试使用的资源有各种优化的途径:

  • 可以优化测试用例,缩短每次执行的时间

  • 可以优化资源调度算法,尽可能地把空闲的资源利用起来,提升资源利用率

  • 问题三:原来一天只测试一次就已经没有时间一一排查失败用例,现在频率更高岂不是没时间排查?
    实际上,排查工作量并不会剧增。因为并不是每次执行的结果都一定要排查。而且,高频测试开始后问题很快就会收敛,所以需要排查的总量并不会增加,反而排查错误的工作量减少

3.2.2 隔离

隔离指不同的测试活动(包括不同的测试运行批次)之间要尽量做到不相互影响。自动化的功能回归测试和其他日常测试不在同一个测试环境中,避免相互的影响

隔离的好处有:

  • 减少噪声
    减少多个测试批次运行时比彼此影响导致的用例失败

  • 提高效率
    会产生全局性影响的测试,可以在多个隔离的环境里并发执行

测试隔离的具体策略要根据技术栈、架构、业务形态来具体分析,选择最合适的技术方案

测试隔离一般分为硬隔离和软隔离两种。硬隔离和软隔离之间并没有一个清晰的界限:

硬隔离
便属物理层,一般通过多实例实现。例如,消息队列和数据库的多个物理实例

软隔离
偏属逻辑层,一般通过多租户和路由实现。例如,同一个消息队列里的不同命名空间、同一数据表里标志位的不同取值、API调用和消息体上的流量标志位等

软隔离的终极形态是Testing in Production(TiP),依靠完善的隔离机制,直接复用生产环境的资源和服务搭建测试环境
TiP是测试环境的终局,这是因为线下环境的某些局限性是无法避免

  • 线下环境的容量有限
    生产环境的容量往往是线下环境的几十、几百倍。仅仅生产环境里的冗余容量和使用率波动的峰谷之间的量,就足以满足对线下环境的容量需求

  • 上下游支持力度不够
    线下环境稳定性经常受到上下游系统稳定性的影响。这些上下游系统也是线下环境,虽然出现问题时报故障处理,但相应和支持力度远不如生产环境。如果把测试环境与上下游系统的生产环境集成,支持力度就不再是问题

  • 线下环境和生产环境不一致
    不一致不仅包括被测系统自身的配置,也包括上下游系统的版本和配置。把被测系统和上下游系统的生产环境集成,能够最大限度减少这种不一致

3.2.3 用完即抛

倡导的一种测试设计原则是"测试环境是短暂的"
一个长期存在的测试环境不可避免地会出现各种问题,比如脏数据、累积的测试数据未清理、配置的偏移等。这些问题严重影响测试的稳定性。虽然可以通过数据清理、配置巡检等手段解决这些问题,但彻底的解决手段是每次都按需创建新的测试环境,用完后即销毁

测试环境用完即抛到好处:

  • 解决环境问题,减少脏数据
  • 提高可重复性,确保每次测试运行的环境都是一致的
  • 倒逼各种优化和自动化能力的建设(测试环境的准备、造数据等)
  • 提高资源使用的流动性。在实际的物理资源不变的前提下,增加流动性可以增加实际容量

测试环境是短暂的则意味:

  1. 测试环境搭建能力要很强。搭建新测试环境要快速、可重复、成功率高、无须人工干预,要能可靠地验证搭建出来的新环境是不是好的、是否满足后续测试的要求
  2. 测试策略和自动化设计必须不依赖一个长期存在的测试环境。例如,不依赖一个长期环境里的老数据进行数据兼容性测试
  3. 日志要打印好。测试环境一旦被销毁,保留下来的就只有应用代码打印的日志和测试自动打印的测试输出。这些日志和输出要详细、清楚,目标是绝大多数的测试用例失败都能通过分析日志定位到问题

测试环境用完即抛的确会引入一些新的质量风险:
如果有一套长期维护的环境,里面的数据是老版本的代码生成的,那么部署新版本的代码后,这些老数据可以帮助发现新代码里的数据兼容性问题
现在用完即抛,没有老数据,这些数据的兼容性问题就可能无法被发现。需要探索数据兼容性问题有没有其他的解法,是否可以通过架构设计解决数据兼容性问题

3.2.4 不自动重跑

很多团队都会在测试平台中增加一个自动重跑的功能:在一次完整的测试执行后,对其中失败的用例自动重跑一遍。如果一个用例的重跑通过,就把这个用例的结果设为通过

这个做法是不好的,自动重跑虽然在短期内好像可以提升测试的稳定性,但长期来看,对整个团队、质量、测试自动化都是有害的

  • 自动重跑会使得工程师不再深入地排查问题,揪出隐藏很深的bug
    因为工程师的时间都是有限的,既然一个用例已经被标为通过,即便只是重跑才通过,但是也没必要仔细看,这样很可能漏过一些真正的bug。比如一个GUID的处理代码,只有在GUID串的第一位是一个特定字符的时候才会触发bug,引起测试用例失败。因此这个测试用例失败概率是1/16,而且在重跑时很大概率是可以通过的

  • 有些用例不稳定的根本原因是系统可测性问题
    有了自动重跑后,反正用例重跑一下还是能通过的,工程师就没有动力改进系统的可测性

  • 需要重跑的用例越来越多,执行所需时间越来越长
    由于深层次的bug没有被揪出来、本质性的问题(比如可测性)没有解决,问题慢慢地积累、蔓延,测试运行后需要重跑的用例越来越多,导致测试所需要的总时间越来越长

对被测系统来说,关闭自动重跑,一开始可能比较痛苦,失败的用例数很多,但只要坚持下去,梅花香自苦寒来

3.3 提升测试的有效性

当测试团队解决了测试的通过率、稳定性、耗时和覆盖率问题后,测试有效性成为下一个需要解决的问题

3.3.1 测试有效性需要面临的挑战

挑战1:注水的成功率和覆盖率

在推动测试的通过率和覆盖率提升过程中,发现个别工程师的测试用例有"注水"行为,例如校验做的不够,该写的assert没写,甚至一些用例无论代码返回什么结构、抛什么错,用例的执行结果都是通过的
必须防止这种注水行为的出现,考虑的解决方法有:

  • 加强代码评审
    但是代码评审需要工程师的时间、能力和责任心。指望代码评审来发现所有的测试有效性问题也不现实

  • 对测试代码进行静态代码分析
    可以识别出被测代码在数据库里,然后解析测试代码,看测试用例是否检查了被测代码库的数据。但这个方法只对一部分的情况比较有效,普适性不强

  • 靠价值观保证,与绩效考评挂钩
    只要发现测试用例有"注水"的情况,写这个用例的工程师考评绩效下调。这个做法的局限性在于有很多漏网之鱼,且等到绩效考核时再改善过于滞后

挑战2:谁来测试测试代码

长期以来,一直困扰软件测试行业的一个问题是:谁来测试测试代码
无论是单元测试,还是端到端的自动化用例,这些测试代码的价值就是让开发人员可以放心修改应用代码。只要测试是通过的,就相信修改后的应用代码是基本正确的
如果想要修改测试代码,怎么保证修改后的测试代码是基本正确的呢?换句话说,如何确保对于被测系统里的任意bug来说,只要修改前的测试代码能报错,修改后的测试代码就能报错
也许可以再写一堆测试代码来测试测试代码,但这堆测试代码又会面临同样的问题。换言之,如何度量测试有效性。如果度量都无法做到,那么更谈不上保障和提高

测试有效性度量的困难有:

  • 传统的测试有效性定义的度量滞后
    传统的测试有效性的定义是:测试有效性 = 测试中发现的bug数/(测试中发现的bug数+交付后发现的bug数)。但是按照这个定义,度量是滞后的,而且由于依赖人工上报、收集bug,这个度量数据存在比较大的偏差

  • 测试有效性不能靠代码覆盖率来度量
    即使修改后的测试代码的代码覆盖率和修改前的代码覆盖率是一样的,也不等于修改后的代码抓bug能力没有下降。即使修改后的测试代码能够正确抓出所有历史上已知的bug,也不代码它对那些尚未发生的bug的捕捉能力和修改前的测试代码是一样的

3.3.2 变异测试和bug注入

在比较各种方案后,选择用变异测试(Mutation Testing)来度量测试用例的有效性

  1. 变异测试的原理

变异测试就是向应用代码中注入一个bug(此处的bug叫做变异),看看测试代码能否发现这个变异,以此来验证测试代码发现bug的能力

例如,想应用代码中注入变异,把b<100改为b<=100。然后针对变异后的应用代码执行一组测试用例。如果其中一个或多个测试用例对之前的应用代码是通过的,但对变异后的应用代码是不通过的,则认为这组测试用例发现了这个bug,这组测试用例对这个bug是有效的、有发现能力的

基于变异测试,对测试用例有效性的定义:
有效性 = 发现的变异数量 / 注入的变异总数

从原理上讲,代码变异主要分为以下三类

  • 第一类称为等效变异
    例如,把a+b变成a-(-b)。这种变异不做,因为它不是一个bug

  • 第二类称为实际等效变异
    例如,把int类型变成short类型,理论上会有溢出的问题。一般情况很少用short类型,只要是整数就直接用int类型,但值的范围无论如何都不会超过几十或几百。比如,变量numberOfDays的值是天数,这种变异不做

  • 剩下的那些变异
    比如算术运算符替换和条件运算符替换,几乎都是bug,很清楚、没争议,主要做的就是这类变异

在实操过程中,常用的变异类型:

  • 算术运算发
    +改成-,++改成-

  • 关系运算符
    !=改成==,>改成<

  • 逻辑运算符
    &&改成||

  • 赋值运算符
    +=改成-=

  • 布尔值
    true改成false

  • 循环结构
    break改成continue,增加break

  • 函数调用
    把setWhitelist(values)改成setBlacklist(values)

  • 删减代码
    把if…else中的else删掉,把try…catch…finally里的catch或finally删掉

  1. 工程化

基于已有的持续集成基础设施,实现了规模化的变异测试

  • 对每个服务的应用代码都自动生成大量不同的变异
  • 对每个变异,都将变异注入应用代码中,结合精准测试的用例筛选执行被度量的测试用例集,根据执行结果判断是否发现了该变异
  • 汇总数据,得到每个服务的测试用例集的变异发现率
  • 每隔一段时间就重复上面这个过程,进行新一轮的度量
  • 合理安排CI调度,见缝插针地利用测试资源使用率较低的时段。尽可能地缩短每轮度量之间的时间间隔
  1. 进一步优化

上述的测试用例有效性度量方案在实际运用中还遇到以下两个难题:

  • 性能:对于几千个应用,如何低成本实现更短的反馈

从注入策略上提升性能。在最初的工程化实践中,模式是将一个变异注入分支,并运行被评估的用例集。如果被测系统的变异点是1000个,那么意味着对一个系统完成一次评估需要执行1000次被评估的用例集。这个成本是非常高的
对此,在注入策略中进行了升级:

a. 多个变异在同一个代码调用路径下,不能同时注入
b. 多个变异在不同的代码调用路径下,可以同时注入
c. 一次执行一次变异和一次执行多次变异进行A/B测试,保障算法的正确性
  • 杀虫剂效应:变异的类型如何不断更新,防止出现杀虫剂效应

软件测试中的"杀虫剂效应"指的是:如果不断重复相同的测试手段,那么软件会对测试免疫,一段时间后,该测试手段将不再能发现新的bug。这是因为多次使用后,在这些测试手段关注的地方和问题类型中,bug已经被修复得差不多了,而且在这些地方, 程序员也会格外关注和小心。最终软件对这些测试免疫
变异测试也会产生杀虫剂效应。例如,如果一直进行>变成>=的注入,那么程序员就会对>和>=的边界条件特别关注。为了避免在变异测试中出现杀虫剂效应,要持续更新变异策略

3.4 提升测试的充分性

围绕把测试做得更好的目标,除了实现更高频持续执行、更高的通过率、更低的噪声、更高的有效性等,还要解决测试遗漏的问题

自动化用例主要靠测试人员凭借自己的测试能力分析来列举,但随着团队规模的扩大,团队人员的测试分析能力参差不齐、测试不充分、遗漏场景和用例,导致出现线上的问题
可以从两个角度来提升测试充分性和解决不知道自己不知道问题:

  • 用例自动生成:用技术手段减少测试人员的个人能力差异的影响,减少对个人经验的依赖
  • 业务覆盖率度量:用技术手段发现测试遗漏,为补充测试用例、提升测试充分性指明方向

3.4.1 用例自动生成

对于人工枚举测试用例来说,一方面人力的多少和水平的高低限制了测试用例的数目和质量,另一方面人工无法穷举所有的输入作为测试用例,无法想到所有可能的业务场景
为了解决这些问题,用例自动生成越来越广泛的研究和使用。用例自动生成的重要作用是减少漏测、提效、节省人力成本

  1. 从测试用例自动生成技术角度看

录制回放是一种运用比较广泛的用例自动生成技术。除了录制回放,还有以下几类用例自动生成技术
a. 基于符号执行
符号执行是一种经典的程序分析技术,以符号值作为输入,而非一般程序执行的具体值
经分析得到路径约束后,通过约束求解器得到客户触发目标代码的具体值
b. 基于模型
这里的模型指能够形式化,程序可以理解和执行的行为描述,包括有限状态机、理论证明、约束逻辑编程、模型检查器、马尔可夫链
目前大多数模型仍需手动创建
c. 基于搜索
通过定义适应度函数,将测试用例自动生成问题转化为目标优化问题,进而用各类搜索算法来解决

  1. 从测试过程的角度看

用例的自动生成包含测试数据的自动生成、测试方法序列的自动生成和测试预言的自动生成
a. 测试数据的自动生成
测试数据包括针对基本类型(如整数型、字符串型)的参数值及针对实体对象类的参数值
针对实体对象类参数生成数据的技术大多局限于通过随机生成方法序列来产生对象状态或基于搜索生成方法序列来产生对象状态
b. 测试预言的自动生成
测试预言的自动生成技术主要通过捕获方法返回值合成回归测试断言(预跑返填)和能推断出运行状态遵循的属性规约的机器学习技术
业界商用工具Parasoft Jtest在自生成测试预言上主要采用捕获方法的返回值合成回归测试断言的技术,只适用于回归测试并且检测bug能力有限的情况。Agitar的AgitarOne主要采用能推断出运行状态遵循的属性规约的机器学习技术,而且需要用户人工甄别工具所推荐的断言为真或假

为解决测试用例和回归用例的覆盖率增长问题,利用自动生成的数据和在线数据,将学习出被测方法的属性规约作为测试预言,然后监控运行产生的测试用例

3.4.2 业务覆盖率度量

如果测试可以覆盖100%的代码(包括行覆盖、分支覆盖、条件覆盖等),是不是就测全,不会遗漏曲线了?答案显然是不。以下面代码为例:

public string Format(int month, int year)
	return string.Format("0/1", month, year);

这段代码要做的事情是把月和年转化为信用卡的过期时间字符串(例如06/2020)
信用卡过期日的格式是MM/YYYY,但对于2020年6月,这段代码返回的是错误的6/2020
正确的写法应该是string.Format("0:00/1:0000", month, year) 。设计这段代码的测试用例如果只测某些业务场景(例如11/2020),就无法发现这个bug

因此,除了度量测试的代码覆盖率,还必须关注业务场景的覆盖率。业务场景覆盖率的定义和业务形态相关度很大。在实际工作中有各种String类型或者复杂数据类型的参数,其取值和值的变化代表了业务对象的状态、状态的转移路径、事件序列、配置项的可能取值组合、错误码和返回码等,有强业务语义并代表了不同的业务场景,需要从业务场景覆盖率角度去度量和覆盖

3.5 从测到不测

做测试不能只把测试做好
做医生的最高境界是不需要治病,打仗的最高境界是不需要打仗。同样的道理,做测试也要思考如何做到不需要测试,只有这样才能变被动为主动,不再跟在项目和需求的后面疲于奔命,面对层出不穷的问题

从测到不测的方法有很多,主要包括下面两类:

  • 防错设计
    在架构、代码、交互等层面优化和加固设计。减少人为错误发生的可能性,或者降低人为错误可能导致的影响

  • 代码扫描
    不写任何测试用例、不执行任何测试,直接对代码进行分析,找到代码中的问题,甚至自动修复bug

3.5.1 防错设计

测试的最终目的是让服务在线上不出问题,少出问题。人为错误是线上问题的一个主要来源

支付系统防错设计的六类常见问题:

  1. 没有第一时间校验输入值
  2. 线上线下权限隔离没做好
  3. 视觉辨识度不佳
  4. 代码容易写错
  5. 事情忘了做了
  6. 事情没有按照正确的方式做

3.5.2 静态代码分析和bug自动识别

  1. 静态代码分析

有很多问题是很难通过测试发现的,但是通过静态代码分析很容易发现和避免,例如:

  • 有业务语义的时间都应该从数据库获取,不可以取本地服务器的系统时间
  • 从数据库获取时间必须使用select now(6),不可以用select now()
  • 避免直接使用get(0)获取集合中的值,集合中元素顺序的改变可能引发问题
  • 存入缓存的对象避免使用Map<String, Serializable>或Map<String, Object>类型,否则可能出现类型转换错误
  • 总是使用Calendar.HOUR_OF_DAY,而避免使用Calendar.HOUR(除非有明确的理由)
  • ORDER BY必须确保排序唯一。例如,当ORDER BY gmt_create ASC存在多条记录gmt_create相等的时候,排序是不确定的。在ORDER BY语句中加入主键字段可以确保其排序是确定的,例如,ORDER BY gmt_create, detail_id ASC中的detail_id为该表主键
  • getAmount().longValue()会丢失金额精度,不应使用
  • 在for循环中,应避免使用RPC方法调用或事物方法
  • ThreadLocal要调用remove(),否则可能导致线程上下文错乱,引发问题

以上这些代码问题类型沉淀为静态代码分析规则,再结合代码规范,用FindBugs、PMD等工具在每次持续集成和使用代码门禁时都对变更代码进行扫描,在第一时间拦截可能有问题的代码

  1. bug自动识别

如果从遇到的实际问题中不断总结积累经验,那么需要的时间比较长,希望有一种方法可以更高效地把过往代码历史中的bug及其修复都提取出来,尽最大可能防止开发人员重蹈覆辙

采取的做法是对代码仓库的历史提交进行聚类和模板提取:

  1. 找到代码仓库中bug修复型commit
  2. 从这些bug修复型commit中以文件级别提取删除内容和新增内容,即bug修复对(Defect and Patch Pair,DP Pair)
  3. 对代码进行格式化和降噪
  4. 对代码进行归一化,解决两个相似的代码块由于变量名或对象名存在差异,而无法聚集到一起的问题
  5. 对bug修复对进行聚类,提取出DP Pair模板,存入模板库

有了模板库,每次新提交代码时,就能扫描代码中是否存在与模板库中的某个DP Pair匹配的代码片段,并给出相应提示,也能根据DP Pair模板给出bug的修复建议

以上是关于测试之道测试发展之路的主要内容,如果未能解决你的问题,请参考以下文章

测试之道测试发展之路

软件测试初学记录——第三章

软件测试--发展之路

测试浅谈(原则简单流程)

微服务架构下的测试之道

软件测试的原则