2023年7月16日发(作者:)
软件测试中的18个难题 对于软件测试来说,怎么样才算测够了?如何评价测试的有效性?那么多测试⽤例,以后怎么删?在软件测试中会遇到⾮常多的问题,阿⾥研究员郑⼦颖分享了18个他总结出的难题以及相关看法,希望对同学们有所启发。⼗多年前我在上⼀家公司的时候看到过内部有个⽹站有⼀个Hard Problems in Test的列表,上⾯⼤概有三四⼗个问题的样⼦,是各个部门的测试同学提供的。但可惜后来那个list失传了,我很后悔⾃⼰当时没有保存⼀份。后来很多次我都想要找到那份list,因为上⾯列的那些问题指出了测试专业在⾃⾝专业性上的巨⼤发展空间。那份list上的问题让当时的我相信,软件测试这件事情本⾝的难度⼀点都不亚于软件开发,甚⾄可能更难⼀点。如果今天要重建这么⼀份Hard Problems in Test列表,下⾯这些问题是我会加到这份列表上的[1]。⼀ 测试充分度(Test Sufficiency)(应⽤情况:冒烟、正逆向、能⼒与功能⼀致性、场景⽤例(未应⽤))如何回答“测够了吗“(包括测新和测旧)。代码覆盖率是衡量测试充分性的起点,但远远不是终点。要回答”测够了吗“,⾄少还要考虑是否测了所有的场景、所有的状态、所有的状态转移路径、所有的事件序列、所有可能的配置、所有可能的数据等等等等。即便如此,我们可能还是⽆法100%确信我们已经测够了。可能我们最终只能做到⾮常趋近于测够了[2]。⼆ 测试有效性(Test Effectiveness)(应⽤情况:⽤例有效性进⾏评估,约为0.3%)如何评价⼀组测试⽤例的发现bug的能⼒。有效性(发现bug的能⼒)和充分性(测够了没有)是两个正交的属性。评价测试⽤例有效性可以通过正向的分析进⾏,例如,分析测试⽤例是否校验了所有在测试过程中SUT落库的数据。更具有通⽤性的做法是变异测试(MutationTesting),即在被测代码⾥注⼊不同的“⼈造bug”,统计多少能被测试⽤例感知到。⽬前变异测试我们已经有⼯程化规模化的落地了,后续的⼯作重点有:1)如何防⽌钝化(或⽈“杀⾍剂效应”),2)不但对被测代码进⾏注⼊,还能对配置、数据等进⾏更全⾯的注⼊。三 测试⽤例瘦⾝(应⽤情况:冒烟⽤例)以前⼴告⾏业有句话:我知道⼴告费有⼀半是浪费掉的,但不知道哪⼀半是浪费掉的[3]。软件测试也有类似的困惑:那么多⽤例,要花那么多时间去跑,我知道这⾥⾯有很多时间是浪费掉的,但我不知道哪些时间是浪费掉的。浪费的形式包括:冗余步骤:有些是浪费在⼀些重复的步骤上,每个⽤例都要去做⼀些类似的数据准备,每个⽤例都要去执⾏⼀些中间过程(这样才能推进到下⼀步)。等价类:⼀个⽀付场景,我要不要在所有的国家、所有的币种、所有的商户、所有的⽀付渠道和卡组的排列组合都测⼀遍?这么测,代价太⾼。不这么测,我担⼼可能某个特定商户在某个特定国家有个特定逻辑我就漏掉了。对于具体的业务,还可以进⾏⼈⾁分析。有没有更通⽤的、⽽且⽐较完备和可靠的等价类分析的技术⼿段?我有N个⽤例,我猜这N个⽤例⾥⾯可能存在M个⽤例,即使删掉这M个⽤例,剩下的N-M个⽤例的效果和之前N个⽤例的效果⼀样。如何识别是否存在这样的M个⽤例、如果存在的话是哪M个。我参加过内部⼀场质量线晋升到P9的评审,当时有个评委问了那位同学⼀个问题:“那么多测试⽤例,以后你怎么删”。这个问题看似简单,其实⾮常难。我觉得,从原理上来说,如果测试充分度和测试有效性的度量都做的⾮常好了、度量成本⾮常低了,我们是可以通过⼤量的不断的尝试来删⽤例的。这是⼀种⼯程化的思路,也许还有其他的理论推导的思路。四 测试分层(应⽤情况:底层接⼝层、协议层、UI层)很多团队都会纠结到底要不要做全链路回归、做到什么程度。这个问题的核⼼点就是:有没有可能、有没有⼀种做法,只要把系统间的边界约定的⾜够好⾜够完整,就可以做到在改动⼀个系统的代码后,不需要和上下游系统进⾏集成测试,只要按照边界约定验证好⾃⼰的代码就可以确保没有任何regression了。包括我在内的很多⼈相信那是可能的,但既⽆法证明,也不敢在实操中就完全不跑集成。我们也缺乏可以完全复制的成功经验,缺乏⼀套完整的⽅法论指导开发团队和QA团队要怎么做就可以达到回归⽆需集成上下游。有时候,我觉得我现在就像是哥德堡的市民,不断的⾛啊⾛,尝试找出⼀条⼀次性不重复的⾛过那7座桥的路线。但也许就有那么⼀天,有⼀个像欧拉那样的⼈会出现在我⾯前,⽤理论证明告诉我,那是不可能的。五 减少分析遗漏(应⽤情况:现场问题反馈)分析遗漏是很多故障的原因。开发做系分的时候,有⼀个corner case没考虑到、没有处理。测试做测分的时候,忘记考虑某个特殊场景了。兼容性评估,评估下来没有兼容性问题的,但结果是有的。⽽且很多时候,分析遗漏属于unknown unknowns,我压根就不知道我不知道。有没有⼀套⽅法和技术,可以减少分析遗漏,可以把unknown unknowns转化为knowns?六 ⽤例⾃动⽣成(应⽤情况:协议⽤例⾃动⽣成,⽤于⽣成冒烟、正逆向、能⼒与功能⼀致性批量⽣成)Fuzz Test、Model Based Test、录制回放、Traffic Bifurcation(引流)等都是⾃动⽣成⽤例的⼿段。有些已经⽐较成熟(例如单系统的录制回放、引流),有些多个团队都在探索(例如Fuzz),有些则⼀直没有⼤规模的成功实践(例如MBT)。我们也有过探索如何从PRD⾥通过NLP来⽣成⽤例。⽤例⾃动⽣成中,有时候难点还不是⽣成test steps,难度反⽽是怎么⽣成test oracle。Anyway,测试⽤例⾃动⽣成是⼀个⾮常⼤的领域,这个⽅向上未来可以做的还⾮常多。七 问题⾃动排查(应⽤情况:未应⽤)包括线上和线下。对于⽐较初级的问题,⾃动排查⽅案往往有两个局限性。⾸先,⽅案不够通⽤,多多少少⽐较定制化。其次,⽐较依赖⼈⼯积累规则(说的好听点叫“专家经验”),主要是通过记录和重复⼈⾁排查的步骤来实现。然⽽,每个问题都不完全⼀样,问题稍微⼀变,之前的排查步骤可能就不work了。现在有⼀些技术,⽐如调⽤链路的⾃动⽐对,对排查问题和缺陷⾃动定位很有帮助。⼋ 缺陷⾃动修复(应⽤情况:⿊盒项⽬、未应⽤)阿⾥的Precfix、Facebook的SapFix等是⽬前⽐较知名的⼀些⼯业界的做法。但总的来说,现有的技术⽅案,都有这样那样的局限性和不⾜,这个领域还在相对早期阶段,后⾯的路还很长。九 测试数据准备(应⽤情况:数据抓包、图像数据采集中)测试⽤例的⼀个重要设计原则是:测试⽤例之间不应该有依赖关系,⼀个测试⽤例的执⾏结果不应该受到其他测试⽤例的执⾏结果(包括是否执⾏)的影响。基于这个原则,传统的最佳时间是确保每个测试⽤例都应该是⾃给⾃⾜的:⼀个⽤例需要触发的后台处理流程应该由这个⽤例⾃⼰来触发,⼀个测试⽤例需要的测试数据应该⾃⼰来准备,等等。但如果每个⽤例所需要⽤到的测试数据都是⾃⼰来从头准备的,执⾏效率就⽐较低。怎么既不违背“测试⽤例之间不应该有依赖关系”的⼤原则,⼜能减少测试数据的准备时间?我设想的是⼀种更加完备的数据银⾏。每个测试⽤例执⾏完后,都会把它⾃⼰产⽣的数据交给数据银⾏,例如,⼀个在某个特定国家的已经通过KYC、已经绑了⼀张卡的会员,⼀笔已经⽀付成功的交易,⼀个已经完成⼊驻签约流程的商户。下⼀个测试⽤例开始的时候,会先问⼀下数据银⾏:“我要⼀个满⾜这样这样条件的商户,你有没有”。上个⽤例跑出来的那个商户正好符合条件,数据银⾏就会把商户“借”给这个⽤例⽤。⽽且⼀旦借出,直到被归还前,这个商户不会被借给其他⽤例。经过⼀段时间的运⾏,数据银⾏能够学习到每个测试⽤例需要什么样的数据、以及会产⽣什么样的数据。这个知识是通过学习得到的,不需要⼈⾁去添加描述,所以也能适⽤于⽼系统的存量⽤例。有了这个知识,数据银⾏可以实现两个优化:⼀次测试执⾏批次开始后,数据银⾏会看到这个批次中后⾯那些⽤例需要什么样的数据,提前先准备起来。这样,等执⾏到那些⽤例的时候,数据银⾏⾥就已经有符合条件的数据准备好了。根据每个测试⽤例需要什么样的数据、以及会产⽣什么样的数据,数据银⾏可以合理的编排测试⽤例的执⾏先后次序,最⼤化的实现测试数据的复⽤,减少测试数据的量和准备开销。测试银⾏把测试数据“借”给⽤例的时候,可以有多种不同的模式。可以是独占(exclusive)的,也可以是共享的。共享的也可以指定共享读、共享写、还是都只读不能写(例如,⼀个商户可以被多个⽤例⽤来测试下单⽀付结算场景,但这些⽤例都不可以去修改这个商户本⾝,例如重新签约)。如果把开关、定时任务等resource也作为⼀种⼴义的测试数据由数据银⾏来管理,能实现测试⽤例尽可能并⾏执⾏。例如,有N个⽤例都需要修改⼀个开关值,这N个⽤例如果并⾏执⾏的话就会相互影响,他们相互之间应该串⾏执⾏。但N个⽤例中的任何⼀个,都可以和这N个⽤例之外的⽤例并⾏执⾏。数据银⾏掌握了每个⽤例对各种资源的使⽤模式的详细情况,再加上每个⽤例的平均运⾏时间等数据,就可以最优化、最准确的对⼀批测试⽤例进⾏编排,做到可以并⾏的都尽可能并⾏、不能并⾏的确保不并⾏,⽽且还可以在⼀个批次的执⾏过程中不断的调整余下还未执⾏的⽤例的编排。这样⼀个数据银⾏是普遍适⽤的,不同业务之间的差异⽆⾮是具体的业务对象和resource不⼀样。这些差异可以通过插件形式实现。如果有这么⼀个通⽤的数据银⾏[4],可以很⽅便的adopt,⼤量的中⼩软件团队的测试效率都可以得到明显的提⾼。这样的⼀个更加完备的数据银⾏的想法,我到⽬前为⽌还只是想法,⼀直没有机会实践。⼗ 异常测试(应⽤情况:混沌⼯程简要应⽤)⼀个分布式系统,它的内部、内部各部分之间以及它和外部的交互都会出现各种异常:访问超时、⽹络连接和耗时的抖动、连接断开、DNS⽆法解析、磁盘/CPU/内存/连接池等资源耗尽等等。如何确保系统的⾏为(包括业务逻辑、以及系统⾃保护措施如降级熔断等)在所有的情况下都是符合预期的?今天我们的线上演练(本质上也是⼀种异常测试))已经做了很多了。如何把更多的问题提前到线下来发现?对于⼀个复杂的分布式系统来说,要遍历所有可能出现异常的地⽅和所有可能出现的异常,异常⽤例的数量是⾮常⼤的。此外,某些异常情况下,系统对外表现出来的⾏为应该没有变化;⽽另⼀些异常情况下,系统⾏为是会有变化的。对于后⼀类,如何给出每⼀个异常⽤例的预期结果(即test oracle),也是⽐较有难度的。⼗⼀ 并发测试(Concurrency Test)(应⽤情况:核⼼功能并发测试)并发(concurrency)可能出现在各个level:数据库层⾯,对同⼀张表、同⼀条记录的并发读写;单系统层⾯,同⼀个进程内的多个线程之间的并发,单服务器上的多个进程之间的并发,以及单个服务的多个实例之间的并发;业务层⾯,对同⼀个业务对象(会员、单据、账户等)的并发操作,等等。传统的并发测试是基于性能测试来做的,有点靠撞⼤运,⽽且经常是即便跑出问题来了也会被忽视或者⽆法repro。并发测试领域,我接触过的⼀些成果包括Microsoft的CHESS以及阿⾥的谭锦发同学在探索的分布式模型检查&SST搜索算法。⼗⼆ 回滚的测试(应⽤测试:反复升降机及参数验证)安全⽣产三板斧宣传了多年,在阿⾥经济体内⼤家都能做到“可回滚”了。但我所观察到的是:很多时候我们有回滚的能⼒,但是对回滚后系统的正确性,事前保障的⼿段还不够。我们更多的是靠灰度和监控等事后⼿段来确保回滚不会回滚出问题来。事实上,过去两年,我⾃⼰已经亲⾝经历过好⼏次回滚导致的线上故障。回滚测试的难度在于:需要覆盖的可能性⾮常多,⼀个发布可能在任何⼀个点上回滚。回滚可能还会引发兼容性问题:新代码⽣成的数据,在新代码被回滚后,⽼代码是否还能正确的处理这些数据。⼗三 兼容性测试(应⽤情况:不同测试对象兼容)代码和数据的兼容性问题有很多形式。例如,如何确保新代码能够正确的处理所有的⽼数据?有时候,⽼数据是⼏个⽉前的⽼代码产⽣的,例如,⼀个正向⽀付单据可能会到⼏个⽉以后才发⽣退款退票。有时候,⽼数据可能就是⼏分钟前产⽣的:⽤户的⼀个操作,背后的流程执⾏到中间的时候代码被升级了。验证这些场景下的兼容性的难度在于:需要验证的可能性太多了。今天的退款请求对应的正向单据,可能是过去很多个版本的代码产⽣的。⼀个业务流程执⾏到中间具体什么地⽅代码被升级了,可能性也⾮常多。异常测试、并发测试、回滚测试、兼容性测试,这些问题的⼀个共同点是:我们知道这些问题是可能存在的,但要测的话,需要测的可能性⼜太多。⼗四 Mock(应⽤情况:mock服务)测试的有效性也依赖于mock的正确性。既然是mock,它和被mock的服务(包括内部的、⼆⽅的和三⽅的)的⾏为就多多少少会有差异。这种差异就有可能导致bug被漏过。前⼈也为此想出了“流量⽐对”等办法。我曾经有另⼀个想法:“⼀鸭三吃”。也就是说,通过bundle和compiler instruction等⽅法,让同⼀套源代码⽀持三种不同的编译构建模式:正常模式:这就是和今天的编译构建是⼀样的,产出的构建物是拿去⽣产环境跑的。Mock模式:这个模式编译出来的就是该服务的⼀个mock,但由于是同⼀套代码编译出来的,最⼤可能的保留了原来的业务逻辑,做到最⼤限度的仿真。⽽且由于是同⼀套代码编译出来的,后期也不会有“脱钩”的担⼼,应⽤代码⾥的业务逻辑变化都能及时反映在mock⾥,⼤⼤减少mock的⼈⾁维护⼯作量。压测模式:这个模式编译出来的也是⼀个mock,但这个mock是⽤来给(上游)做性能测试⽤的。过去在线下的性能压测中经常遇到的情况是:我们想要压的系统还没到瓶颈,这个系统的下游系统(往往是⼀个测试环境)反⽽先到瓶颈了。压测模式编译出来的这个mock牺牲了⼀部分的业务逻辑仿真,但能确保这个mock本⾝性能⾮常好,不会成为性能瓶颈(但对lantency仍然是仿真的)。这个“⼀鸭三吃”的想法so far还停留在想法层⾯,我还⼀直没有机会实践⼀下。⼗五 静态代码分析(应⽤情况:⿊盒,未进⾏)有⼀些类型的问题,要⽤通常意义上的软件测试来发现,难度和成本很⾼,但反⽽是通过静态代码分析来发现反⽽⽐较容易。例如,ThreadLocal变量忘记清除,会导致内存溢出、会导致关键信息在不同的不同的上游请求之间串错。另⼀个例⼦是NullPointerException。⼀种做法是通过fuzz testing、异常测试等⼿段来暴露代码⾥的NPE缺陷,以及可以在执⾏测试回归的时候观察log⾥⾯的NPE。但我们也可以通过静态代码分析,更早的就发现代码⾥⾯可能存在的NPE。有⼀些并发问题也可以通过静态代码分析来早期准确发现。总之,我们希望尽可能多的通过静态代码分析来防住问题。⼗六 形式化验证(Formal Verificaition)(应⽤情况:业务分析)除了在协议、芯⽚、关键算法等上⾯的运⽤以外,形式化⽅法在更偏业务的层⾯是否有运⽤的价值和可能?⼗七 防错设计(Mistake Proof)(应⽤情况:未应⽤)严格来说,防错设计并不是software testing范畴内的。但做测试做久了就发现,有很多bug、很多故障,如果设计的更好⼀点,就压根不会发⽣(因此也就谈不上需要测试了)。去年我总结了⼀下⽀付系统的防错设计,后⾯希望能看到在各类软件系统形态下的防错设计原则都能总结出来,另外,最好还能有⼀些技术化的⼿段来帮助更好的落地这些防错设计原则,这个难度可能⽐总结设计原则的难度更⾼。⼗⼋ 可测性(Testability)(应⽤情况:提出接⼝⽅法完整⽅案、⽤于可测试性提升)虽然⽬前⼤部分开发和QA同学都知道“可测性”这么件事情,但对可测性把握的还不够体系化,很多同学觉得可测性就是开接⼝、加testhook。或者,还没有很好的理解可测性这个东西落到⾃⼰这个领域(例如⽀付系统、公有云、ERP)意味着什么。在需求和系统设计分析阶段还不能做到很有效很有体系的从可测性⾓度提出要求,往往要求⽐较滞后。我希望可测性设计可以总结出⼀系列像程序设计的DRY、KISS、Composition Over Inheritance、Single Responsibility,Rule of Three等设计原则,总结出⼀系列的反模式,甚⾄出现像《设计模式》那样的⼀本专门的著作。以上就是我会加到Hard Problems in Test列表的问题,也是我已经或打算投⼊精⼒解决的问题。注:[1] 我⼯作中还有⼀些其他的测试难题,那些问题在这⾥没有列出来,因为那些问题和特定的业务场景或者技术栈的相关度⽐较⾼。还有⼀些测试领域的挑战,难度也很⾼,例如,回归测试达到99%以上通过率、主⼲开发以及做到通过代码门禁的code change就是可以进⼊发布的,这些也⾮常有难度,但难度主要是是偏⼯程的⽽不是软件测试技术本⾝。[2] 测试充分度的度量和提升是两个问题。有⼀种观点认为,测试充分的度量和提升其实是⼀件事情,⽤同样的算法分析数据可以进⾏度量,也能⽤同样的算法来基于数据进⾏测试充分度的提升。我不同意这个观点。度量和提升未必是同⼀个算法。这样的例⼦⾮常多了:测试有效性的度量和提升、运维稳定性的度量和提升,等等。即便度量和提升可以⽤同⼀种算法,我也希望可以尽量再找⼀些其他⽅法,尽量不要⽤同⼀种算法⼜做度量⼜做提升,因为这样容易“闭环”和产⽣盲区和。[3] 当然,这句话今天可能不再是那样了,但那是⼗⼏年前,那时候的在线⼴告和⼤数据还没到今天这个⽔平。[4] 具体形式上,这个数据银⾏⽆需是⼀个平台。它不⼀定是⼀个服务,它也不⼀定需要有UI。它可以就是⼀个jar包,它可以就是在测试执⾏时launch的⼀个单独的进程。
发布者:admin,转转请注明出处:http://www.yc00.com/xiaochengxu/1689458783a251625.html
评论列表(0条)