
1.1 开发者与软件测试
开发者需要对自己开发的程序代码承担质量责任。在软件质量管理机制下,一般要求开发者首先自行对自己编写的代码进行审查和测试,并保证提交的代码已达到一定的质量标准。开发者测试中的单元测试和集成测试主要采用白盒测试方法,要求测试人员对软件代码非常熟悉。这样的测试任务由软件开发人员来做效率会更高。
1.1.1 测试和调试
在软件开发过程中,开发者需要对程序进行测试和调试。测试和调试极其相关但含义完全不同。简单来说,测试是为了发现缺陷,调试是为了修复缺陷。调试往往需要依赖已有的测试信息或者补充更多测试信息,需要先找出缺陷根源和缺陷的具体位置,再进行修复以消除缺陷。而从职责上说,测试只需要发现缺陷,并不需要修复缺陷。在软件开发过程中,开发者需要同时肩负这两种职责,对自己开发的程序进行测试,发现缺陷并对其进行调试以修复缺陷。调试的过程如图1-1所示。

图1-1 调试的过程
调试时如果已经识别或者找到测试中所发现缺陷的产生原因,就可以直接修复,然后进行回归测试。如果没有找到缺陷的产生原因,可以先假设一个最有可能的原因,并通过附加测试来验证这样的假设是否成立,直到找出原因为止。
调试工作是程序员能力和水平的一个重要体现。软件开发调试有时难度很大,原因如下:1)失效症状和缺陷原因可能相隔很远,高度耦合的程序结构加重了这种情况;2)失效症状可能在另一缺陷修复后消失或暂时性消失;3)失效症状由不太容易跟踪的人为错误引发;4)失效症状可能是由不同原因耦合引发的。因此,程序员有时会因为在调试程序时找不到问题所在,而使软件开发工作陷入困境。
程序调试方法多种多样,更多时候是依赖程序员的经验及其对程序本身的理解。调试方法的具体实施可以借助调试工具来完成,如带调试功能的编译器、动态调试辅助工具“跟踪器”、内存映像工具等。
回溯法是指从程序出现不正确结果的地方开始,沿着程序的运行路径向上游寻找错误的源头,直到找出程序错误的实际位置。例如,程序有5000行,测试发现最后输出的结果是错误的,采用回溯法,可以先在第4500行插桩,检查中间结果是否正确。若正确,则错误很可能发生在第4500~5000行之间。若不正确,则在第4000行插桩,以此类推,直到找出程序错误的具体位置。
1.1.2 开发者测试
从履行职责、提高效率、保护源代码、方便实现等角度来说,开发者需要完成的测试工作主要集中在单元测试和集成测试阶段。程序代码开发出来之后,开发者先对自己开发的代码进行单元测试,然后再把多个已经通过单元测试的模块按照设计书组装起来进行集成测试。当然,在实践中也会出现后期的系统测试需要开发者配合甚至主导的情况。
静态测试与动态测试都是开发者需要掌握的测试方法,在实践中一般将两者结合起来使用。开发者对自己开发的程序代码进行检查,这是静态测试;而开发者运行代码,给定输入数据,检查程序能否正常运行并给出预期结果,则是动态测试。对静态测试存疑的代码部分,加强动态测试进行结果验证,是实践中常用的开发者测试策略。
白盒测试是开发者最主要的测试方法,也是在软件测试工作上体现开发者优势的地方。然而,并不是说开发者测试不需要黑盒测试方法。恰恰相反,我们建议开发者在实践中对程序代码进行等价类和边界值分析,这有助于提高开发者测试的效率和质量。而在集成测试或者配合一些复杂模块的测试中,开发者也可能会用到灰盒测试等方法。
在开发者测试中,手工测试与自动化测试都会用到。随着软件技术的发展,软件测试的自动化程度会越来越高。开发者在测试中应尽可能通过自动化测试工具来提高测试工作效率。但并不是所有测试工作都能够自动化完成,也不是所有场景都适用自动化测试。
为了提高软件质量和缩短软件项目总工期,测试常常与开发同步进行。研发人员应综合运用多种软件测试方法和技术,针对不同的测试场景合理选择测试方法和工具,并在测试时尽可能采用自动化测试工具来提高软件测试的效率。
总体上,开发者测试一般采取先静态后动态的组合方式:先进行静态结构分析、代码评审,再进行代码的覆盖性测试。利用静态分析的结果作为导引,通过代码评审和动态测试的方式对静态测试结果进行进一步的确认,使测试工作更为有效。代码覆盖测试是白盒测试的重点,通常可采用语句覆盖、分支覆盖等。对于软件的重点模块,可使用逻辑覆盖、路径覆盖、数据流覆盖等更复杂的准则。在不同的测试阶段,测试重点有所不同。在单元测试阶段,以代码审查和语句覆盖为主;在集成测试阶段,需要增加接口测试和模块集成结构分析等。
1.1.3 PIE模型
软件测试的主要目的之一是发现缺陷。动态测试工作中通常会出现复杂而有趣的现象。假设某个程序中有一行代码存在缺陷,在该软件的某次运行中,这个存在缺陷的代码行并不一定会被执行。即使这存在缺陷的代码被执行,若没有达到某个特定条件,程序状态也不一定会出错。即只有在运行错误代码,并达到某个特定的条件,程序状态出错并传播出去被外部感知后,测试人员才能发现程序中的缺陷。
软件测试的一个基本模型称为PIE(Propagation-Infection-Execution)模型。PIE模型对于理解软件测试方法、测试过程、缺陷定位和程序修复等都具有重要作用。在介绍PIE模型之前,我们首先理解缺陷在不同阶段的不同名称及其含义:
·Fault(故障):故障是指静态存在于程序中的缺陷代码,有时也称之为程序缺陷(Defect)。
·Error(错误):错误是指程序运行缺陷代码后导致的错误状态。
·Failure(失效):失效是指程序错误状态传播到外部被感知的现象。
针对缺陷不同阶段的性质,我们可以构建一个PIE模型来解释缺陷产生的整体过程。PIE提示我们,发现一个缺陷需要满足以下三个必要条件:
1)Execution(运行):测试必须运行到包含缺陷的程序代码。
2)Infection(感染):程序必须被感染出一个错误的中间状态。
3)Propagation(传播):错误的中间状态必须传播到外部并被观察到。
上述三个条件是缺陷被检测出来的必要条件,三个必要条件组合成检测出缺陷的充分条件,即充要条件。我们不难理解,一个测试满足条件1不一定能满足条件2,即测试运行到包含缺陷的代码,但不一定能感染出错误的中间状态。一个测试满足条件2不一定能满足条件3,即测试能感染出错误的中间状态(当然也运行到了包含缺陷的代码),但不一定能成功传播出去并被测试人员发现。
为了进一步理解这些现象,以图1-2中的示例程序MY_AVG来进行说明。程序语句s4存在缺陷,循环控制变量i的初值应为0,而不是1。

图1-2 一个示例程序MY_AVG
在程序的某次运行中,调用了上述代码段MY_AVG,并输入测试数据0,4,5,预期(正确)输出结果是3。程序运行到了s4(条件1满足),中间变量V_sum应该为0+4+5=9。由于缺陷导致数组第一个数字“0”被遗漏,中间变量V_sum为4+5=9(条件2不满足)。但由于0的累加不影响最终结果,最终的平均值为3,与预期输出结果一致。
我们将上述程序做一下微调,缺陷依然是循环初值1,如图1-3所示。

图1-3 简单修改后的示例程序MY_AVG
在程序的某次运行中,调用了上述代码段MY_AVG,并输入测试数据4,3,5,预期(正确)输出结果是4。程序运行到了语句s4(条件1满足),中间变量V_sum应该为4+3+5=12。由于缺陷导致数组第一个数字“4”被遗漏,中间变量V_sum为3+5=8(条件2满足)。但个数变量n的累加也减少了1,即n原来应该为3,现在是2(条件2满足)。这两个错误的中间状态叠加后,V_avg=12/3=8/2=4,导致最终结果正确(条件3不满足)。
PIE模型表明发现Bug并不是一件容易的事情。要全面发现软件Bug,不仅需要针对特定需求和软件特性进行测试设计,还需要学会利用不同的软件测试方法,如有效地结合使用白盒测试和黑盒测试方法。