1 怎么描述软件的 Bug?
为什么软件里会有 Bug?我们一定会在第一时间想到,这可能是因为程序员在写程序时犯了错误。那么从程序员在程序中写下错误的代码,到最终观察到软件运行出现错误的结果,这中间经历了什么样的过程呢?人们定义了 PIE 模型来描述这样一个过程。
在学习派模型之前,我们首先需要明确一些相关的名词和概念,我们经常会用各种不同的词汇来描述软件中出现的 bug。例如我们可能会听到类似这样的描述:
- 这段程序里有 Bug
- 程序运行时出现了错误
- 这是一个存在缺陷的软件
- 系统一旦失效会造成严重的后果
- 这个软件需要确保长时间无故障运行
在这里 Bug 错误缺陷,故障失效,这些词汇在概念上都有什么区别?它们的内涵和外延分别是什么?如果再考虑那些相关的英文词汇,例如 defect、fault、error,fail、failure 等等,这一问题就会变得更加复杂。
2. 三个重要概念的辨析
所以首先我们需要对这些似是而非的概念做一个简单的梳理。这里我们主要参照了 H5E1044标准,以及《Introduction to Software Testing》这本经典教材中的描述,着重考虑与 Bug 相关的三个概念,fault、error 和 failure。

参考标准:H5E1044标准 和《Introduction to Software Testing》
fault 是指存在于软件当中的静态缺陷,这里我们特别强调静态,它是由于程序员在编码的过程当中犯了一些错误,这些错误给程序留下了一些隐患,但是这个隐患是否一定会造成不良的后果,就像一个有缺点的人,并不一定会干坏事儿,一辆有设计缺陷的汽车,也不一定会出交通事故一样。
假如在程序运行过程当中, fault 触发了一个错误的中间状态,这里所谓的错误中间状态,我们可以理解为某个变量被赋予了一个错误的值,那么这种状态我们就称之为 error 。
failure 在中文里我们通常称之为失效,是指软件的用户或是测试人员在外界观察到了程序未能完成预期的行为。
2. 通过代码理解 Bug 相关概念
下面我们通过一些简单的案例来帮助大家理解 fault、error 和 failure 这三个概念之间的区别和联系。这里有一段程序,它的功能是读取一个存放了若干整数的 STL 动态数组,并计算这个动态数组当中所有元素的平均值。
很多以 C 语言或是 Java 语言作为入门语言的人,在初学程序设计语言时,都曾经犯过搞错数组下标范围的错误。他们要么认为一个长度为 n 的数组当中的第一个元素的下标是 1 而不是 ,要么会认为一个长度为 n 的数组元素中的最后一个元素的下标是 n 而不是 n-1。于是他们在使用 for 循环来遍历这个数组时,就可能会写出这样两种错误的代码:

除此之外,还有些人可能会犯另外一个错误,我们注意到这段程序的返回值类型是 float,也就是说在求数组平均值时需要考虑平均值不是整数的情况。所以在最后一行语句中,我们需要使用一个强制类型转换,有些人可能会忘记这一点,我们忘了在 return 语句当中去加强制类型转换,所以这里他们所进行的除法就是整数与整数之间的除法,那么最终的结果也会是一个整数。
不管是在 for 循环当中写错了下标范围,还是漏写了 return 语句当中的强制类型转换,这里我们都可以说程序员在这个程序当中留下了静态的缺陷,也就是 fault。
我们选择三个带有 fault 的程序版本当中的其中之一去运行。这里不妨选择第一个错误版本,假设输入的数组所含的元素依次是 3 2 1,那么在正确的版本中 for 循环的循环体会执行三次,变量 sum 的值依次是 3 5 6,而在错误版本当中,for 循环的循环体只执行两次,每次执行后 sum 的值依次是 2 3。这里我们就说 sum 变量出现了错误的中间状态,也就是 error。
程序继续执行后续语句,最终正确版本的函数和错误版本的函数分别会返回浮点数 2.0 和 1.0,错误程序输出了一个错误的计算结果,所以说我们说它失效了,也就是 failure 。
3. 由 fault 到 failure 的三个必要条件
现在我们回到最初的问题,从程序员在程序中写下错误的代码,也就是 fault,到最终观察到软件运行出现错误的结果,也就是 failure ,这中间经历了什么样的过程呢?
从 fault 到 error 到 failure,是否需要什么特殊的条件呢?为了刻画从 fault 到 error 到 failure 的这样一条因果链,人们提出了 PIE 模型。派模型当中的 PIE 这三个字母分别是三个单词的首字母:Execution、Infection 和 Propagation。

首先需要第一个条件,也就是执行(Execution),包含错误的代码必须被执行,才有可能造成程序的失效。如果错误的代码不被执行,它也就不会带来什么不良影响。来看数组起始索引为 1 的错误版本:

也就是说这里的
fault,即i = 1必须要被执行到。如果我们的测试用例为v ={ }那么就执行不到for循环的i = 1。
第二个条件我们称之为感染(Infection)。这个 failure 被执行后,必须触发出一个错误的中间状态,如果错误的代码被执行后,所有的中间状态都是正常的,那么也不会带来什么不良影响。还是这个错误版本:

要导致触发
error,那我们观测的中间变量sum必须产生错误的状态。如果测试用例中的第一个值为,那么中间值sum就不会出现错误的状态。
第三个条件,我们要求错误的中间状态,应该能够随着程序的执行一路传播下去,直到被外界观察到程序实际运行结果和预期结果的不一致,也就是失效(Failure)。我们考虑在 return 语句当中遗漏了强制类型转换的错误版本:
![[E-附件/PIE模型.png]]
也就是说,我们最后 return 的这个均值,需要和正确的程序不同。如果测试用例为
3 2 1,最后没有做强制类型转换,return 的值也是一样的,这就 没有导致failure。
之所以需要这三个条件,是因为在很多情况下,一个测试用例可能并不会执行包含错误的代码,而执行了错误代码的测试用例,可能也不见得会触发一个错误的中间状态。最后错误的中间状态可能也没有办法最终传播到输出模块。
4. 可达性(Reachability)
我们用以上三个案例分别展示了派模型当中执行、感染、传播这三个条件。事实上,这些条件并不很容易满足。为了让程序当中的错误能够尽量地暴露出来,我们希望错误的语句都能够被执行,但这一定是可行的吗?我们可能会遇到一些情况,在这些情况里面,这些包含错误语句的位置,可能根本就没有机会被执行,例如下面这段程序:

我们发现在这段程序当中,Fun1() 这个函数它就是没有机会被执行的,因为执行到这个函数所需要的条件和程序入口处的条件是矛盾的,这样的语句我们就称之为不可达语句。所以在派模型的三个条件当中,对于执行的这样一个条件,有时又会附加一个条件,我们称之为 Reachability,也就是可达。
学完了这一点,MOOC 当中给出 Quiz 也可以回答了:

答案是:是的。如果这个 fault 在不可达语句中,那么它将不能被任何测试用例测试到,但它依然是 fault,例子为上面给出的程序。
总结: PIE 模型 = Execution (+ Reachability) + Infection + Propagation
5. 练习 (必考题)
