十多年前我在上一家公司的时候看到过内部有个网站有一个Hard Problems in Test的列表,上面大概有三四十个问题的样子,是各个部门的测试同学提供的。但可惜后来那个list失传了,我很后悔自己当时没有保存一份。后来很多次我都想要找到那份list,因为上面列的那些问题指出了测试专业在自身专业性上的巨大发展空间。那份list上的问题让当时的我相信,软件测试这件事情本身的难度一点都不亚于软件开发,甚至可能更难一点。
如果今天要重建这么一份Hard Problems in Test列表,下面这些问题是我会加到这份列表上的[1]。
一 测试充分度(Test Sufficiency)
如何回答“测够了吗“(包括测新和测旧)。代码覆盖率是衡量测试充分性的起点,但远远不是终点。要回答”测够了吗“,至少还要考虑是否测了所有的场景、所有的状态、所有的状态转移路径、所有的事件序列、所有可能的配置、所有可能的数据等等等等。即便如此,我们可能还是无法100%确信我们已经测够了。可能我们最终只能做到非常趋近于测够了[2]。
二 测试有效性(Test Effectiveness)
如何评价一组测试用例的发现bug的能力。有效性(发现bug的能力)和充分性(测够了没有)是两个正交的属性。评价测试用例有效性可以通过正向的分析进行,例如,分析测试用例是否校验了所有在测试过程中SUT落库的数据。更具有通用性的做法是变异测试(Mutation Testing),即在被测代码里注入不同的“人造bug”,统计多少能被测试用例感知到。目前变异测试我们已经有工程化规模化的落地了,后续的工作重点有:1)如何防止钝化(或曰“杀虫剂效应”),2)不但对被测代码进行注入,还能对配置、数据等进行更全面的注入。
三 测试用例瘦身
以前广告行业有句话:我知道广告费有一半是浪费掉的,但不知道哪一半是浪费掉的[3]。
软件测试也有类似的困惑:那么多用例,要花那么多时间去跑,我知道这里面有很多时间是浪费掉的,但我不知道哪些时间是浪费掉的。浪费的形式包括:
- 冗余步骤:有些是浪费在一些重复的步骤上,每个用例都要去做一些类似的数据准备,每个用例都要去执行一些中间过程(这样才能推进到下一步)。
- 等价类:一个支付场景,我要不要在所有的国家、所有的币种、所有的商户、所有的支付渠道和卡组的排列组合都测一遍?这么测,代价太高。不这么测,我担心可能某个特定商户在某个特定国家有个特定逻辑我就漏掉了。对于具体的业务,还可以进行人肉分析。有没有更通用的、而且比较完备和可靠的等价类分析的技术手段?
- 我有N个用例,我猜这N个用例里面可能存在M个用例,即使删掉这M个用例,剩下的N-M个用例的效果和之前N个用例的效果一样。如何识别是否存在这样的M个用例、如果存在的话是哪M个。
我参加过内部一场质量线晋升到P9的评审,当时有个评委问了那位同学一个问题:“那么多测试用例,以后你怎么删”。这个问题看似简单,其实非常难。我觉得,从原理上来说,如果测试充分度和测试有效性的度量都做的非常好了、度量成本非常低了,我们是可以通过大量的不断的尝试来删用例的。这是一种工程化的思路,也许还有其他的理论推导的思路。
四 测试分层
很多团队都会纠结到底要不要做全链路回归、做到什么程度。这个问题的核心点就是:有没有可能、有没有一种做法,只要把系统间的边界约定的足够好足够完整,就可以做到在改动一个系统的代码后,不需要和上下游系统进行集成测试,只要按照边界约定验证好自己的代码就可以确保没有任何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的服务(包括内部的、二方的和三方的)的行为就多多少少会有差异。这种差异就有可能导致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同学都知道“可测性”这么件事情,但对可测性把握的还不够体系化,很多同学觉得可测性就是开接口、加test hook。或者,还没有很好的理解可测性这个东西落到自己这个领域(例如支付系统、公有云、ERP)意味着什么。在需求和系统设计分析阶段还不能做到很有效很有体系的从可测性角度提出要求,往往要求比较滞后。我希望可测性设计可以总结出一系列像程序设计的DRY、KISS、Composition Over Inheritance、Single Responsibility,Rule of Three等设计原则,总结出一系列的反模式,甚至出现像《设计模式》那样的一本专门的著作。
以上就是我会加到Hard Problems in Test列表的问题,也是我已经或打算投入精力解决的问题。