对于软件测试,什么是足够的测试?如何评价测试的有效性?这么多测试用例,以后怎么删除?在软件测试中你会遇到很多问题,阿里研究员郑子英分享了他总结的18个疑难问题及相关观点,希望对同学们有所启发。十多年前我在上一家公司的时候,看到一个内部网站,上面有一个HardProblemsinTest的列表。上面大概有三十、四十道题,都是各个系的考生提供的。但遗憾的是那个名单后来遗失了,很遗憾当时没有留一份。后来多次想找那个榜单,因为上面列举的问题点出了测试职业在自身专业性上的巨大发展空间。那个清单上的问题当时让我确信,软件测试本身的难度不亚于软件开发,甚至可能更难一点。如果我今天要重建这样一个HardProblemsinTest列表,以下问题将添加到此列表[1]。1.测试充分性(TestSufficiency)如何回答“测试够不够?”(包括新试和旧试)。代码覆盖率是衡量测试充分性的起点,但离终点还很远。要回答“测试够了吗?”,至少要考虑是否所有的场景、所有的状态、所有的状态转移路径、所有的事件序列、所有可能的配置、所有可能的数据等都已经测试过了。即便如此,我们可能无法100%确定我们已经进行了足够的测试。也许我们最终只能做非常接近于足够的测量[2]。二测试有效性(TestEffectiveness)如何评价一组测试用例发现bug的能力。有效性(发现错误的能力)和充分性(你测试得够多了)是两个正交的属性。评价测试用例的有效性可以通过正向分析来进行,例如分析测试用例在测试过程中是否验证了SUT中存储的所有数据。一种更通用的方法是MutationTesting,它将不同的“人工错误”注入被测代码,并计算测试用例能够感知到的程度。目前,我们已经进行了工程化和大规模的变异测试。后续工作主要集中在:1)如何防止钝化(或“杀虫剂效应”),2)不仅注入被测代码,还注入更全面的配置、数据等。三个测试用例瘦身有一个以前在广告界有句话说:我知道广告费有一半是浪费的,但我不知道浪费的是哪一半[3]。软件测试也有类似的困惑:那么多用例,跑起来要花那么多时间,我知道浪费了很多时间,却不知道浪费了哪些时间。浪费的形式包括:冗余步骤:有的浪费在一些重复的步骤上,每个用例都要做一些类似的数据准备,每个用例都要进行一些中间过程(这样才能推进到下一步)。等价类:一个支付场景,是否要在所有国家、所有币种、所有商户、所有支付渠道、卡组组合上测试?这样的测试成本太高了。如果我不这样衡量,我担心特定的商家可能在特定的国家有特定的逻辑,所以我错过了。针对特定业务,还可以进行人肉分析。有没有更通用、更完善、更可靠的等价类分析技术手段?我有N个用例,我猜想这N个用例中可能有M个用例,即使把这M个用例删掉,剩下的N-M个用例的效果和前面N个用例的效果是一样的。如何识别是否有这样的M个用例,如果有是哪M个。我参与了质量线提升到P9的内部审查。当时有个法官问同学一个问题:“这么多测试用例,以后怎么删?”这道题看似简单,其实很难。我认为,原则上,如果测试充分性和测试有效性的度量做得很好,并且度量成本很低,我们可以通过大量的不断尝试来删除用例。这是一个工程思路,可能还有其他的理论推导思路。4.测试分层很多团队会纠结到底要不要做全链路回归,做到什么程度。这个问题的核心点是:是否可以,有没有办法,只要系统之间的边界足够好,足够完整,就可以实现一个系统改了代码之后,就不用再改了。与上下游系统集成测试,只要你按照边界约定来验证你的代码,就可以保证没有回归。包括我在内的很多人都认为这是可能的,但既不能证明也根本不敢在实践中运行集成。我们也缺乏可以完全复制的成功经验,缺乏一套完整的方法论来指导开发团队和QA团队如何在不整合上下游的情况下实现回归。有时,我觉得我现在是哥德堡的市民,走啊走,试图找到一条一次性的、不重复的路线,穿过7座桥。但也许有一天,像欧拉这样的人会出现在我面前,用理论证明告诉我这是不可能的。5.减少分析遗漏分析遗漏是许多失败的原因。在开发和做部门积分的时候,有一个没有考虑和处理的cornercase。在测试和评分时,忘记考虑特殊场景。兼容性评估,评估后没有兼容性问题,但结果是有。而很多时候,分析遗漏属于不为人知的未知数,甚至不知道自己不知道。是否有一套方法和技术可以减少分析遗漏,将未知的未知转化为已知?六大用例自动生成FuzzTest、ModelBasedTest、录音回放、流量分叉(引流)等都是自动生成用例的手段。有的比较成熟(比如单系统录音回放、引流),有的正在多个团队探索中(比如Fuzz),有的还没有大规模实践成功(比如MBT)。我们还探讨了如何通过NLP从PRD生成用例。在用例自动生成中,有时难点不在于生成测试步骤,而在于如何生成测试oracle。不管怎么说,测试用例的自动生成是一个非常大的领域,未来在这个方向上还是有很多可以做的。七种自动故障排除包括在线和离线。对于相对初级的问题,自动故障排除解决方案通常有两个局限性。首先,解决方案不够通用,或多或少是定制化的。其次,它更多地依赖于规则的人工积累(说得好听一点,叫“专家经验”),主要是通过记录和重复人肉筛选的步骤来实现的。但是,每个问题都是不同的。如果问题稍有变化,则之前的故障排除步骤可能不起作用。现在有一些技术,比如调用链接的自动比对,对故障排除和缺陷自动定位有很大的帮助。八、缺陷自动修复阿里巴巴的Precfix、Facebook的SapFix等都是业界比较知名的一些做法。但总的来说,现有的技术方案存在着各种局限性和不足。这个领域还处于比较早期的阶段,还有很长的路要走。九测试数据准备测试用例的一个重要设计原则是:测试用例之间不应存在依赖关系,一个测试用例的执行结果不应受到其他测试用例执行结果(包括是否执行)的影响。基于这个原则,传统的最佳时机是保证每个测试用例都应该是自给自足的:需要由用例触发的后台处理过程应该由用例本身触发,而需要的测试数据由用例触发。自己准备一个测试用例。etc.但是如果每个用例需要的测试数据都是从头准备的,那么执行效率会比较低。如何在不违背“测试用例之间不应存在依赖性”的原则的情况下,减少测试数据的准备时间?我设想一个更完整的数据库。每个测试用例执行后,其产生的数据都会交给数据库,例如某个特定国家通过KYC并绑定卡的会员,支付成功的一笔交易,已完成登录过程。下一个测试用例开始时,首先会询问数据库:“我想要一个满足某某条件的商户,你有吗?”上一个用例出来的商户刚好符合条件,数据库就会把商户“借”给这个用例。并且一旦借出,商户在归还之前不会再借给其他用例。经过一段时间的运行,数据库可以了解到每个测试用例需要什么样的数据,会产生什么样的数据。这些知识是通过学习获得的,不需要人肉添加描述,所以也可以应用到老系统的存量用例中。有了这些知识,数据库可以实现两个优化:在一个测试执行批次开始后,数据库会看到这个批次中后面的用例需要什么样的数据,并提前准备好。这样,当那些用例被执行时,数据库中已经有符合条件的数据准备好了。根据每个测试用例需要什么样的数据,会产生什么样的数据,数据库可以合理安排测试用例的执行顺序,最大限度地复用测试数据,减少测试数据量和准备成本。测试库将测试数据“借”给用例时,可以有多种不同的模式。它可以是独占的或共享的。shared也可以指定共享读,共享写,或者既只读又不可写(比如一个商户可以被多个用例用来测试订单支付结算场景,但是这些用例不能修改商户本身,例如重新合同)。如果将开关、定时任务等资源也作为广义测试数据由数据库管理,则可以尽可能并行执行测试用例。比如有N个用例需要修改一个switch值,这N个用例并行执行会相互影响,应该串行执行。然而,N个用例中的任何一个都可以与N个用例以外的用例并行执行。数据库掌握了每个用例对各种资源的使用模式的细节,再加上每个用例的平均运行时间等数据,可以优化和最准确地安排一批测试用例,从而可以并行的尽可能并行,不并行的保证不并行,剩余未执行的用例的编排可以在一个batch的执行过程中不断调整。这样的数据库是普遍适用的,不同业务之间的区别无非就是具体业务对象和资源的区别。这些差异可以以插件的形式实现。如果有这样一个通用数据库[4],就可以很容易地采用,大量中小型软件团队的测试效率可以得到显着提高。这么一个更完整的数据库的想法至今只是一个想法,一直没有机会去实践。十个异常测试一个分布式系统,它的内部、内部部分,以及它与外部的交互都会出现各种异常:访问超时、网络连接耗时抖动、连接断开、DNS无法解析、磁盘/资源等CPU/内存/连接池耗尽等,如何保证系统的行为(包括业务逻辑,降级熔断等系统自我保护措施)在所有情况下都符合预期?今天我们的线上演练(本质上是异常测试))已经完成了很多。如何提前在线下发现更多问题?对于一个复杂的分布式系统,需要遍历所有可能的异常和所有可能的异常,异常用例的数量非常多。此外,在某些异常情况下,系统的外部行为不应发生变化;而在其他异常情况下,系统行为会发生变化。对于后一类,如何给出每个异常用例(即测试oracle)的预期结果也比较困难。十一并发测试(ConcurrencyTest)并发可能出现在各个层面:数据库层面,同一张表,同一条记录的并发读写;单系统层面,同进程多线程间并发,单台服务器多进程间并发,单个服务多个实例间并发;在业务层面,对同一业务对象(成员、文档、帐户等)的并发操作,等等。传统的并发测试是基于性能测试,有点侥幸心理,往往即使出了问题,也会被忽视或无法重现。在并发测试领域,我接触到的一些成果包括微软的CHESS和阿里的谭金发正在探索的分布式模型检查&SST搜索算法。十二回滚测试安全生产推进多年,阿里经济体人人都能实现“回滚”。但是我观察到的是:很多时候我们有回滚的能力,但是回滚后系统的正确性并不足以保证系统提前的正确性。我们更多的是依靠灰度、监控等事后手段来保证回滚不会出问题。其实这两年,我亲身经历过几次因为回滚导致的线上故障。回滚测试的难点在于需要涵盖的可能性太多,发布可能随时回滚。回滚还可能导致兼容性问题:新代码回滚后,新代码产生的数据是否能被旧代码正确处理。十三兼容性测试代码和数据兼容性问题有多种形式。例如,如何确保新代码能够正确处理所有旧数据?有时,旧数据是由几个月前的旧代码生成的。例如,一份远期付款文件可能要到几个月后才能退还。.有时,旧数据可能是几分钟前产生的:用户的一次操作,中间执行后面的进程时,代码被升级了。在这些场景中验证兼容性的困难在于验证的可能性太多。今天的退款请求对应的forwarddocument,可能是过去很多版本的代码生成的。代码在业务流程执行过程中升级的可能性有很多种。异常测试、并发测试、回滚测试、兼容性测试,这些问题有一个共同点:我们知道这些问题可能存在,但是如果要测试的话,测试的可能性太多了。十四Mock测试的有效性还取决于mock的正确性。由于它是一个模拟,它的行为或多或少与模拟服务(包括内部、第二方和第三方)不同。这种差异有可能导致错误被遗漏。前人为此还想出了“流量对比”等方法。我曾经有过另一个想法:“一鸭三餐”。也就是说,同一套源码通过bundle、compilerinstruction等方式,支持三种不同的编译构建模式:普通模式:这个和今天的编译构建是一样的,输出构建到生产环境跑步。Mock模式:该模式编译一个服务的mock,但是因为是从同一套代码编译而来,所以最大程度的保留了原有的业务逻辑,做到了最大程度的模拟。并且因为编译的是同一套代码,后期不用担心“解耦”。应用代码中的业务逻辑变化可以及时反映在mock中,大大减少了mock的人工维护工作量。压力测试模式:该模式编译一个mock,但是这个mock用于(上游)性能测试。以往离线性能压测经常遇到的情况是:我们要压的系统还没到瓶颈,但是这个系统的下游系统(往往是测试环境)先到了瓶颈。压测方式编译的mock牺牲了部分业务逻辑模拟,但是可以保证mock本身的性能非常好,不会成为性能瓶颈(但还是针对lantency进行了模拟)。“一鸭三餐”的想法至今还停留在想法层面,还没有机会去实践。15静态代码分析有一些类型的问题,通常意义上的软件测试很难发现,成本也很高,但是通过静态代码分析更容易发现。比如忘记清除ThreadLocal变量,会导致内存溢出,导致不同上游请求之间的关键信息被误解。另一个例子是NullPointerException。一种方法是通过模糊测试、异常测试等方式暴露代码中的NPE缺陷,在进行测试回归时观察日志中的NPE。但我们也可以通过静态代码分析,更早发现代码中可能存在的NPE。一些并发问题也可以通过静态代码分析来及早准确地发现。简而言之,我们希望通过静态代码分析尽可能多地预防问题。16形式化验证(FormalVerification)除了协议、芯片、关键算法的应用,形式化方法在更面向业务的层面上是否还有使用价值和可能性?十七MistakeProof严格来说,防错设计一般不属于软件测试的范畴。但是做了很久的测试,发现bug和失败的地方很多。如果设计更好,则根本不会发生(因此无需测试)。去年总结了支付系统的防错设计。后面希望看到能总结出各种软件系统形态的防错设计原则。另外,最好有一些技术手段,帮助更好的实现这些防错设计原则的难度,可能比总结设计原则更难。第十八篇可测试性(Testability)虽然大部分开发和QA同学都知道“可测试性”,但对可测试性的把握还不够系统。很多同学认为可测性就是开放接口,加测试hook。或者,对于可衡量性落到我自己的领域(比如支付系统,公有云,ERP)是什么意思不太理解。在需求和系统设计分析阶段,无法从可度量性的角度有效、系统地提出需求,需求往往滞后。希望可测试性设计能总结出程序设计中DRY、KISS、CompositionOverInheritance、SingleResponsibility、RuleofThree等一系列设计原则,总结出一系列反模式,甚至像《设计模式》那样出现。这本特别的书。以上是我将添加到HardProblemsinTest列表中的问题,也是我已经或打算投入精力去解决的问题。注:[1]我的工作中还有一些其他的测试问题,那些问题这里就不一一列举了,因为那些问题更多的是和具体的业务场景或者技术栈相关。测试领??域也有一些挑战,难度也很大。例如通过率99%以上的回归测试、骨干开发、通过代码权限控制的代码变更,才能进入发布。这些也都非常难,但难的主要是部分工程而不是软件测试技术本身。[2]衡量和提高测试的充分性是两个问题。有观点认为,测试充分性的衡量和提高其实是一回事。使用相同的算法分析数据可以用于测量,可以使用相同的算法来提高基于数据的测试充分性。我不同意这个观点。指标和提升不一定是相同的算法。这样的例子还有很多:测试有效性的衡量和提升,运维稳定性的衡量和提升等等。即使可以用同一个算法来衡量和改进,我也希望尽可能的找一些其他的方法,尽量不要用同一个算法来衡量和改进,因为很容易“闭环”,生成盲区。[3]当然,这句话在今天可能不一样了,那是十多年前的事了,那时候网络广告和大数据还没有今天的水平。[4]在具体形式上,该数据库无需为平台。它不一定是服务,也不一定需要有UI。可以是jar包,也可以是测试执行时启动的单独进程。
