从直观上说所有真正的检验都是试图给理论“下绊子”:它不但是一场严格的考试,而且是一场不近人情的考试——其目的在于使考生们落败考场,而不是给考生们以机会去展示他们所知道的东西。展示所知的态度是那些打算去确证或“证实”其理论的人的态度。

——卡尔•波普尔(Karl Popper)[1]

证实与证伪

人们通常把软件测试视为一种归纳并推论过程,尤其是在功能性测试(functional testing)方面,我们借助边界值、等价类、决策表等测试设计方法,通过设计与执行部分用例来推断整体。比如,对于一个只接受1~255的输入,如果按照边界值的方法,我们选择检验1、2、128、254、255,即可推断程序是否运行正确。也就是说在这里说我们通过观察1/51场景的测试结果,来推论50/51未经测试场景的结果。

这样的测试必然是不完备的,从逻辑的观点来看,显然不能证明从单称陈述(某个场景没问题)中推论出全称陈述(所有场景都没问题)是正确的。就像我们不能从单称陈述“昨天太阳从东方升起”或“今天太阳从东方升起”来推论全称陈述“每一天太阳都从东方升起”一样。在上例中,实际上我们是可以对255个场景进行完备的测试的,但对于绝大多数需要测试的软件系统,测试工作者面对的往往是无穷的场景。现在让我们考虑一个很简单的功能:用鼠标点击屏幕上的一个按钮,鼠标按键松开后系统弹出OK的提示。在不检查实现代码的情况下,我们并不能保证在按钮的不同坐标位置点击后会出现同样的结果,也不能保证从按下到松开的不同延时会出现同样的结果,另外,即使是同样的坐标、同样的延时,要预期第一次点击与第二次点击会出现同样的结果似乎也是武断的。尽管这些条件看上去有些不切实、甚至带有诡辩的味道,但从实现的角度来讲,这些都是有可能的:因为编程人员完全可以在代码中人为地设置一个让人意想不到的复杂条件,来触发一个后门、或者一个非常规事件——但它却是再多的测试都不一定能发现的。因此,不管是操作步骤与操作环境的复杂组合,还是道德风险的可能性,都使得能证明软件有缺陷的潜在场景已近乎无限大。这也就意味着:即使只是单纯地针对软件中某一个功能做完全验证,也是几乎不可能的。

而在早先的测试文献中我们也不难找到类似的论述,比如在Weinberg 1986中,杰拉德•温伯格(Gerald Weinberg)就指出我们无法测试所有的可能:“首先,人类的大脑不仅会犯错误,而且它的容量也是有限的。其次,没有人的生命可以无限长。所以无论我们多想进行所有可能的测试,也无法全部考虑到;即使全部考虑到了,也无法活得足够长来将它们进行完” [2]。而在Patton 2006中,罗恩•巴顿(Ron Patton)也强调了完全测试程序是不可能的,主要原因是“输入量太大;输出结果太多;软件执行路径太多;软件说明书是主观的” [3]。在这些早先的文献中,都如上文一样,强调了因为操作组合与环境组合的难以穷尽给质量证明所带来的困难。然而从哲学的视角看,导致潜在场景难以穷尽的主要因素还并非海量的操作组合与环境组合,而是时空。因为从时空的角度看,即使是对一特定操作之结果的论断也是一个全称陈述。也就是说诸如“在设备A上运行程序,并在按钮B的坐标(15,15)上点击鼠标,系统显示结果C”这样一个陈述实际上就已包含了无穷多个可能的场景,因为它断定该陈述在一切时空中都为真。但这显然是无法证实的,因为即使我们昨天观察到它为真、今天观察到它为真,甚至过去数百次观察都是如此,我们也仍不能断言在下次观察中必然为真。因此我们可以说,近乎无穷的操作组合与环境组合使得我们事实上无法证明软件的完美,但真正无穷的时空因素却是使得这类证明在理论上就已不可能的根本原因。

软件测试并不能证明软件是完美的,不管受测的软件是多么简单都是如此,当我们从时空的维度来重新审视软件时,我们感受到了归纳的无力,在实证领域,不存在所谓的归纳。就像观察到数以亿计的人说汉语,也无法确保所有的人都说汉语一样——在时空的无限论域中,一种假设哪怕是得到了再多的经验证据支持,其逻辑概率也永远趋于零。所以,我们不可能通过测试来宣称或者断言软件的完美。当然,这并不是说世界上不可能存在完美的软件,而是只说——我们有可能开发出了完美的软件而不知。

与证实全称陈述的复杂性和不可能相比,要证伪一个全称陈述却是相对的简单,因为尽管有再多的观察也不能推论出全称陈述为真,但反过来,只要存在一例观察不为真我们便推倒了整个全称陈述。这就是哲学家卡乐•波普尔(Karl Popper)所说的证实与证伪之间存在的“令人震惊的不对称”:“单一的观察陈述集合有时可以证伪或反驳一个全称定律,但它不可能在确立定律的意义上证实定律。这个事实的另一个表述为:单一的观察陈述集合可以证实一个存在陈述(这意味着证伪一个全称定律),但无法证伪该存在陈述。这是基本的逻辑情境,它表明了一种令人震惊的不对称” [4]。当我们证伪一个全称陈述时,实际上是证实了“存在着某一例观察结果与全称陈述与推论不符”。在软件测试中,如果我们拒绝“某软件是完美”这一论述,那么必然是我们证实了在该软件中至少存在着一例缺陷。我们将诸如“软件存在缺陷”、“上帝是存在的”与“世间有黑天鹅”等称之为存在陈述,这些陈述在形式上都是存在主义的。与全称陈述的不可证实但可被证伪相反,存在陈述是可证实但不可被证伪的。在理论上,我们要证实这些陈述是有可能的(至少是潜在可能),但却无法否定它们。也就是说,即使某人终此一生都未目睹过上帝,又或者我们之前所见到的一万只天鹅全都不是黑色的,我们也不能反驳它们的存在;与之相反,哪怕是只要有一例观察与陈述相符,这些陈述就得到了证实。因此,在关于软件缺陷的论述中,我们可以证实某个软件确实存在缺陷,但却无法证伪它:即证明其“不存在”缺陷、或者认为它是完美的。事实上,我们甚至不需要进行任何的测试,也能大胆地去评价任何软件、断定它们“存在缺陷”,而不用担心会遭到何种反驳——因为存在陈述是不可证伪的。

这是一个有趣的结论。如果软件测试工作者的任务是判断软件是否有缺陷的话,那么可以说这份工作既是最容易的,也是最困难的。它的容易之处在于我们总是可以无任何后顾之忧地给出存在缺陷的判断,而困难则是我们永远也无法通过努力来证明软件无缺陷。在现实世界中,人们往往会寄望测试能证明软件的完美,或者让测试工作者确保此时此地检验了的测试场景,在彼时彼地也能按预期执行,但这却恰恰是测试最不可能完成的任务。

测试与决定

测试工作者在测试过程中所得到的发现与知识,实际上既不足以证明软件的完美,也不足以决定后续的行动。测试是一种批判、一种实验,如果说实验是一切知识的试金石,那么测试就是我们对软件所宣称的质量的试金石。测试能告知我们的是:软件经受了何种广度与强度的检验,以及它在什么场景下存在着缺陷,但也仅此而已。我们通过测试来获取信息,而非给出预言。相反,测试恰恰是对来自开发人员对软件质量的预言(这个软件没问题)的检验与批判。正如在物理学中的分工那样(理论物理学家进行想象、推演和猜测新的定律,但并不做实验;而实验物理学家则进行实验、想象、推演和猜测),测试工作就是想尽一切办法去寻找问题、去让软件失败,除此无他,与证明扯不上半点关系。努力去维护受测系统、去证明我们的软件是多少的正确,这从来就不是可取的测试精神,测试工作者永远不会说受测软件是无缺陷的,而只会说这软件目前通过了眼下的检验,我们还没有发现否定它的理由。然而这一肯定的判决也只能是暂时的,我们仍对随后的否定判决开放。因此,测试可以看作是一个探索的过程、一个批判的过程,它获取信息,而不做出决定。测试不负责决定软件是否可以发布、也不负责决定我们是否要取消某个项目,甚至都无法决定接下来是否还需要进一步的测试。测试过程中所产生的信息,对于这些决定来说是重要的、也是关键的,但我们也必须承认,这些信息并不充分。

如果说测试永远是不完备的,只能告知我们当下软件经过了何种程序的检验,那么,对于软件发布后它在客户处的表现、会不会出现问题、以及会出现什么问题,理论上我们都只是推断与猜测。在这一过程中,我们通过已检验的场景来推断未经检验的场景,通过这一时空的检验结果来推断另一时空的执行结果。在做出是发布软件还是继续测试的决定时,实际上我们面临着一种两难,因为这里面存在着两类可能的错误:第一类错误是软件还有未找出的缺陷但我们过早地发布了,而另一类错误则是推迟发布但没并没有找出新的缺陷。但是遗憾的是,在这两类可能的错误之间,我们别无选择,只能选择尽力避免其一,而不能选择两者都避免。因为我们越是试图控制一类错误的发生概率,反过来就越是加大了犯另一类错误的可能性。

在统计概念中,这称之为假设检验。在进行假设检验时,我们将设置一个假设为原假设,用H0表示,在这里它表示

H0:软件还存在可能找出的缺陷

第二个假设称为备选假设或研究假设,用H1表示。在决定是否应该继续测试时,它表示

H1:软件不存在可能找出的缺陷

如此一来,我们设置了两个互为矛盾的假设,在真实世界里只能有其一为真。然而,就像前文中所提到的,我们有可能开发出完美的软件而不知;因此一般来说,我们并不知道、也不能断定哪个假设是正确的,我们只能猜测。当我们进行假设检验时,实际上会存在两种可能的错误。第一类错误是当原假设正确时,我们却拒绝了它。第二类错误被定义为当原假设有错误时,我们却没有拒绝。在上面的例子中,第一类错误就是进一步测试还能发现更多的问题(H0为真),但我们却匆忙发布了软件(拒绝了H0);而当我们为了保险进行了一些冗余测试(拒绝了H1),但并没有发现问题时(H1为真),第二类错误就发生了。我们把发生第一类错误的概率记为α,通常它也被称作显著水平;第二类错误发生的概率记为β。发生错误的概率α和β是相反的关系,这就意味着任何尝试减少某一类错误的方法都会使另外一类错误发生的概率增加。在决定是发布还是继续测试时,如果我们试图减少第一类错误(还能进一步找到缺陷,但没有继续测试),我们就必须进行更多更完备地测试,但这一决定无疑就增加了犯第二类错误(做了更多的测试,但没有进一步发现缺陷)的可能性。反过来也是如此,当我们修复一个局部缺陷而不进行完整测试的时候,实际上我们是希望避免发生第二类错误的可能性,但这样做我们就肯定增加了犯第一类错误的可能性。

第一类错误(匆忙发布软件)的成本是客户故障成本、商誉蒙尘或法律风险等,而第二类错误(推迟软件的发布)的成本则可能是冗余的测试成本、商机延误或未能尽早地得到市场反馈。有些产品犯第一类错误的成本较高,比如航天软件与医疗软件,因此人们往往会进行更多更完备的测试,宁愿做了许多事后看来无谓的测试、犯第二类错误,也要全力避免第一类错误发生。另外一些软件则相反,比如互联网产品、或是一些市场处于生命周期早期的产品,在这些领域,商机往往是瞬息万变、稍纵即逝的。在此若是因为犯第二类错误而导致商机延误,与因第一类错误引起的软件故障相比可能会严重得多。因此,综合来看,需要做多完备的测试、何时发布软件?这实际上是判断哪一类错误更严重的问题,不同的行业会有不同的选择,不同的竞争者也会有不同的选择,而测试本身并不足以做出这一选择。这里面涉及到的商业价值与成本评估问题,是市场工作者的领域,测试可以提供信息,但绝不能越俎代庖。从上述角度推论,我们可以认为在软件领域里,单从测试角度是不足以决定软件是否可以发布的,同时也不存在通用的软件发布标准。

测试与系统

软件系统无疑是复杂的,作为一个复杂系统,我们无法证明其正确性。这一点与自然界规律是相似的,也正因为自然界的复杂性与不可知,对于自然科学的规律,我们只能证伪不能证实。然而,尽管同是因为复杂性导致了软件行为与自然界行为的不可证实,但这两类系统的复杂性来源与复杂程度却是不同的,认识到这点将有助于我们进一步理解软件测试。经济学家肯尼思•博尔丁(Kenneth Boulding)根据复杂程度,将系统分为九大类型[5]:

静态架构 拥有静态结构,如晶体里原子的排列或动物的骨骼。

时钟结构 具有预定运动模式的简单动态系统,如钟表和太阳系。

自调系统 具有根据某些外给目标或准则自我调节的能力,如恒温器。

开放系统 能够通过从环境获取资源进行自我维系,如生物细胞。

衍生系统 不是通过复制,而是通过含有成长指令的种子或卵进行繁衍,例如鸡和蛋系统。

内像系统 具有获取信息详尽地认识环境的能力,以及把这些认识组织成关于环境整体形象或知识结构的能力,这是动物能力所能达到的层次。

抽象系统 具有自我意识,能够运用语言,人类行为处于这一层次。

社会系统 由具有第7层次能力并遵循共同的社会秩序与文化的行动者组成。

绝对和必然不可知的系统

博尔丁的分类给我们许多启发。首先,它展示了世界上系统存在的广泛性与多样性。其次,我们在上述分类中可以看出软件系统与自然系统之间的不同:它们分别属于第3层与最为复杂的第9层。

作为第9层的自然系统,其复杂性在于,我们对于它内部机制与目标完全不可知。无论科学如何发展,科学都不是一个确定的或即成的陈述系统,也不是一个朝着一个终级状态稳定前进的系统,而仅仅只是关于真实世界的猜想。已故的诺贝尔物理学奖得主理查德•费恩曼(Richard Feynman)有一个关于自然系统的生动比喻:”可以作一想象:组成这个世界的运动物体的复杂排列似乎有点像是天神们所下的一盘伟大的国际象棋,我们则是这盘棋的观众。我们不知道弈棋的规则,所有能做的事就是观看这场棋赛。当然,假如我们观看了足够长的时间,总归能看出几条规则来……除了我们还不知道所有的规则外,我们真正能用已知规则来解释的事情也是非常有限,因为几乎所有的情况都是极其复杂的,我们不能领会这盘棋中应用这些规则的走法,更无法预言下一步将要怎样。” [6]大自然的复杂性与内在机制的不可知使得我们一切有关这个世界的知识都只是猜测,而证实与证伪之间惊人的不对称又使得我们即使找到了真理的时候也无法知道这一点。因此对于自然科学,波普尔说:“我们不知道,我们只能猜测”。

与自然系统相比,软件系统有着与之截然不同的复杂性来源。作为人类科技与意志的产物,我们并非对其内部机制与目的不可知,相反,是我们设计了它的内部机制与目标,可以说,我们是它的造物主。但我们人作为造物主,与自然系统万能的造物主相比,又是极为渺小和有限的。在构造一个有大量执行路径的软件系统的过程中,我们难免会想当然、会忘记、会迷糊、也会疏忽,甚至还有可能存在道德风险,这些都使得我们不应当将软件视为一个必然会遵从设计与预期的系统,它是复杂的、不确定的,因而也是需要测试与批判的。一方面,是人无与伦比的智慧赋予了它遵从预期行动的能力,另一方面,也是人不可避免的局限性为其埋下了可能偏出预期的种子。也就是说,其行为的不可预知不在于机制的不可知,而是我们并不能保证我们能完全不出错地将设计的机制转换为软件所预期的行为。正是因为这种结果的不可保证性,我们才需要测试——我们设计了内部机制但无人能保证它的实现。

这就是软件系统的两面性,一方面,我们不能因为知其既定的机制,就确信它一定能按照这些机制来实现其行为,因而我们需要怀疑、需要忘掉这些机制去测试它的各种输入与输出,黑盒测试在任何时候都是必不可少;另一方面,我们又不能将其看作更高复杂层次的系统,假装我们对其内部机制一无所知。如此一来,我们实际上是完全放弃了原本可以从内部理解其行为的便利,而这样的测试设计必然是欠缺效率的,因而从白盒的角度分析软件系统也是同样必不可少。

今天,关于软件的复用,我们已经取得了长足的发展。从函数级复用到组件级复用、再到框架级复用,软件复用一方面缩短了软件系统的开发周期,另一方面也减少了缺陷的引入。显然,如果我们不关心软件的内部机制的话,那么测试人员将不能从软件复用中得到任何好处:因为站在黑盒的角度,我们不能对软件的实现作任何假设,理论上我们必须测试一切窗口控件,即使这些窗口控件是复用的,早已经过了千锤百炼;我们也必须测试一切底层API,即使这些底层API都是标准的系统调用。但是,如果我们能正视这些机制、利用这些机制的话,我们的测试将更有针对性,也更有效率。我们不应该认为测试应该从零开始,就像我们不应该认为一切科学理论都应该从零开始一样。在科学的各个领域里,往往都只存在着少数的一些基本的公理,在这些少数公理的基础上,科学家们借助大量的次级理论构建出了巍巍的科学大厦。因此,绝大多数的软件系统与科学理论一样,实际上都不是孤立地存在的,它们都包含了一些暂时经受了批判的部分。当我们检验系统时可以选择将这一部分看作是背景知识,暂时地接受,而将精力更多地集中在检验那些更脆弱、更容易发现缺陷的部分。

小结

在经典著作《人类理解研究》中,哲学家大卫•休谟曾如此设问并回答:“我以前所食的那个面包诚然滋养了我,那就是说,具有那些可感性质的那个物体在那时候,是赋有那些神秘的能力的。但是我们果能由此推断说,别的面包在别的时候也一样可以滋养我、而且相似的可感性质总有相似的神秘能力伴随着它么?这个结论在各方面看来都不是必然的。”[7]休谟因而指出了归纳的局限性,即我们不能因为曾经看到那样一个物象总有那样一个结果伴随着它,就可以预测相似的物象也会有相似的结果。在大自然中,这种不可预测性是因为自然系统的不可知。而对于软件测试,尽管我们知道不少,但我们也不能因观察到软件系统的某一功能在此时此地没问题,因而断定在客户处也必然是没问题的。在要求对事物做出准确预测的时候,只要我们没有把握到事物的必然性,那么不管是对事物内部有一知半解还是完全一无所知,实际上是没有区别的。因此,在探讨关于测试我们知道什么这一问题时,我们大可将软件视为实现上不可知的系统,这是测试的本质,也是测试工作的起点。然而,当我们探讨如何去测试与批判一个系统时,则将软件系统视为在一定程度上是可知的又是非常有必要的,因为这是关于效率的问题。因此,我们如何看待受测系统,取决于我们需要回答或解决的是哪类问题。但无论如何,软件测试都是批判而非证明:从不可知的观点出发,我们不能证明;而从有限可知的观点看,又有太多的东西可以不去证明。忙乱地写着冗余的测试用例的测试人员,就像是在路灯下寻找车钥匙的人一样,我们不能说他错了,但他肯定不可能是卓有成效的。优秀的测试工作者是将测试看作是批判、是艺术的人,他们勇敢地承认自己的局限性,卸下了证明的重负,站在巨人的肩上,然后看得更远。

注释

[1]: Karl Popper. 《Realism And The AIM of Science》. 1983.

[2]: Gerald M. Weinberg. 《Perfect Software: And Other Illusions about Testing》. 1986.

[3]: Ron Patton. 《Software Testing》. 2006.

[4]: Karl Popper. 《Realism And The AIM of Science》. 1983.

[5]: Kenneth E. Boulding. 《General System Theory: The Skeleton of Science》. Management Science, 2. 1956.

[6]: Richard Feynman. 《The Feynman Lectures On Physics, Volume I》. 1963.

[7]: David Hume. 《An Enquiry Concerning Human Understanding》. 1927.