给自己代码把关的开发者,常常有这么一种“测过了”的体验:把功能从头点一遍,页面出来了、接口返回 200、数据库里也写进去了,于是觉得没问题了。但测试真正要回答的问题不是“它跑起来了吗”,而是“它做的事,和它该做的事一致吗”。这两句话差得很远。差在哪里,就是这篇要讲清楚的事。
测试的动作只有三步,关键是第三步
把一次测试拆开,只有三个动作:给程序一个输入,观察它实际做了什么,再把实际结果和一个预期结果做比较。不一致,就说明找到了问题。
最容易被忽略的是第三步。程序不会“自己发现错误”,它只是产出行为——返回一个值、写一条记录、弹一个提示。是“比较”这一步把行为变成了结论。没有预期,测试就无从判断:一个接口返回 {"code": 0},如果你不知道它应该返回什么,这个 0 既不算对,也不算错。
所以测试的难点从来不只是“怎么把程序跑起来”,而是“凭什么说它现在这样是错的”。
缺陷是从哪儿来的
业界把问题的来龙去脉分成三段,串成一条链子 1:
- 错误:人的一次失误。比如需求写的是“年满 18 周岁可注册”,开发者写成了
age > 18。 - 缺陷:留在工作产品里的那个瑕疵。工作产品指的是开发过程中产出的任何东西,不只是代码,需求文档、设计文档、测试用例都算——一份含糊不清的需求描述,本身就是缺陷。
- 失效:程序运行时表现出的、与预期不符的现象。用户输入 18 岁被拒绝注册,这就是失效。
为什么要费劲区分这三层?因为测试能直接观察到的只有第三层。你看到的是“18 岁被拒绝了”,看不到代码里那个 >。从失效倒推缺陷,是调查和调试的工作,不是测试本身。
围绕这条链,有两个反直觉的地方值得记住。
第一,缺陷可以长期不产生失效。没被执行到的那段代码里就算有缺陷,它也不会自己冒出来。
第二,失效不一定来自缺陷。网络断了、依赖的服务挂了,同样会表现为“程序不对”,代码里却可能一点瑕疵都没有。
预期从哪来:测试预言
第三步里那个“提供预期、用来判断对错的东西”,叫测试预言(test oracle)3。名字听着玄,其实就是判断依据:它回答“按什么标准,这次结果算对”。
来源可以有很多:需求文档里写死的规则、上一个版本的行为、同类产品的通行做法,或者有经验的人的判断。写用例时那句“预期结果:提示年龄不符合要求”,就是把预言固化下来,让它可复用、可争论。
真正的麻烦是:预言几乎不可能完整。它只会盯着自己被设定去看的那几个方面。断言写成“接口返回 200 且 code 为 0”时,提示文案写错、金额算错、慢得离谱,这条用例都不会响。所以更准确的说法是:通过一条用例,你得到的结论是“它没有在我盯着的地方失败”,而不是“它一定对” 4。测试研究把“怎么判断实际结果是不是对的”称为预言问题(oracle problem),它至今仍是自动化测试里最难的环节之一。
需求本身也可能是缺陷
回到注册的例子。需求写着“年满 18 周岁可注册”,开发者照写,测试照测,全部通过。可如果真实业务规则其实是 16 岁呢?程序完全符合需求,上线后用户照样用不了。
这就要引入一对必须分开的词 5:
- 验证(verification):我们有没有把产品造对。对照的是需求、规格和设计文档。
- 确认(validation):我们造的是不是对的产品。对照的是真实的使用需要。
Boehm 的概括常被引用:verification 问的是“有没有把产品造对了”,validation 问的是“造的是不是对的产品”。两者谁也替代不了谁。日常说的“测试”,默认基本落在验证这一层;而真正致命的问题,常常出在确认这一层——它靠对着需求文档比对结果是发现不了的,必须回到需求本身、回到用户和业务方那里去问。
一条用例通过,到底说明了什么
把前面几节合起来,一条成功通过的用例,你最多能说:
在这一次运行里,用这组输入、这个环境,程序的行为没有偏离我用来比较的那个预期。
它不能说明的,至少有这几条:
- 其它输入没问题。你只试了这组输入。
- 没执行到的代码没问题。缺陷可以安静地待在无人踩到的路径里。
- 预期本身是对的。如果预期写错了,用例“通过”只说明程序和你犯了同一个错。
- 程序符合真实需要。那是确认层面的问题。
Dijkstra 在《Notes on Structured Programming》里写过一句被反复引用的话:程序测试可以用来证明 bug 的存在,但永远不能证明 bug 不存在 2。他的理由是,程序可能的执行路径太多,测试只能覆盖其中极小一部分,没走到的地方有没有问题,测试回答不了。而预言不完整是另一层原因:即使真的走了那条路径,你手里那把用来判定对错的尺子本身也可能是错的。
举个例子。需求是“金额输入框只允许两位小数”。你测了 0.01、1.99、100.00,全通过。你能说这个输入框没问题吗?不能。负数、极大值、空字符串、前后空格、全角数字、超长输入都没试过;而且“只允许两位小数”这条要求本身该不该允许负数,也从来没人检查过。前者是没覆盖到,后者是没验证预期本身。
落到手上的三个动作
第一,每条用例都写下预期结果。 只有操作步骤、没有预期结果的用例,产出的结论无法判定,本质上不算测试。
第二,写完用例回头问一句:这个预期是哪来的? 最靠得住的来源是需求或验收标准。如果依据只是“我觉得应该这样”,要么去找人确认,要么在缺陷单里把依据写清楚——依据站不住,报出来的问题就很难被接受。
第三,分清自己在查哪一层。 对照需求测功能,是验证;判断需求是否真的对,是确认。后者不是测试一个人能拍板的,但当你发现需求有歧义、有遗漏、和业务方的说法对不上时,应该提出来——需求文档里的缺陷和代码里的缺陷一样值钱,而且发现得越早越便宜。
下一篇会接着这里往下走:既然“预期”是判断对错的前提,那需求到底怎么一步步变成可以测的预期和测试点。