软件测试技术
原载 中国软件评测师联盟
软件测试的重要性及其对软件质量的好坏的预意是非常重要的。下面这段话引自Deutsch[DEU79]:
软件系统的开发包括一系列生产活动,其中由人带来的错误因素非常多。错误可能出现在程序的最初…,其时目标可能是错误的或描述不完整,也可能在后期的设计和开发阶段…,因为人们不能完好无缺地工作和交流,软件开发过程中必须伴有质量保证活动。
软件测试是软件质量保证的关键元素,代表了规约、设计和编码的最终检查。
软件作为系统元素的可见性不断增加软件故障带来的代价太高使得人们注重于规划良好的彻底测试,软件开发组织将30%—40%的项目精力花在测试上并不为怪。另一方面,人命悠关的软件(如飞行控制和核反应堆)测试所花的时间往往是其他软件工程活动时间之和的三到五倍。
以下讨论软件测试基础和设计软件测试用例的技术。软件测试基础定义了软件测试的目标,测试用例的设计讨论符合整体目标的测试用例创建技术。
1.1软件测试基础
测试为软件工程师带来了很有趣的意外。在软件过程的早期,软件工程师试图由抽象概念到具体实现来建立软件,现在来了测试,工程师创建测试用例试图“摧毁”已经建立的软件。事实上,在软件工程过程中,测试可以看成(至少心理上)摧毁性的而不是建设性的。
软件开发者就其本性而言是建设者,测试要求开发者放弃刚开发的软件是正确的观念,并克服发现错误时的心理矛盾。Beizers[BEI90]如下描述了这种情况:
如果我们真正擅长编程,就应当不会有错误,但这只是一个神话。如果我们真的很认真,如果每个人都使用结构化方法,自顶向下设计而且使用决策表,如果程序是用SQUISH写的,如果我们有合适的银弹,就不会有错误了,这样,神话就不存在。因为我们并不擅长所做的事,所以有错误,如果不擅长,就应当感到内疚。因此,测试和测试用例设计是对失误的承认,它注入了一针内疚剂。测试的枯燥是对我们错误的处罚,为什么处罚?为了人?为什么内疚?为了没能达到人类的完美境界?为了没有区别另一个程序员所想的和所说的?为了没有心灵感应?为了没有解决人类四千年来尚未解决的相互通信问题?
测试真的应当注入内疚感?测试真的是摧毁性的?这些问题的回答是“不!”,然而,测试的目标可能和我们所期待的不同。
1.1.1测试目标
Glen Myers[MYE79]在他的软件测试著作中陈述了一系列关于测试目标的规则:
1.测试是一个为了寻找错误而运行程序的过程。
2.一个好的测试用例是指很可能找到迄今为止尚未发现的错误的用例。
3.一个成功的测试是指揭示了迄今为止尚未发现的错误的测试。
上述目标蕴含了一个观点上的戏剧性变化,他们转向通常的观点,即一个成功的测试是指没有找到错误的测试。我们的目标是设计这样的测试,它们能够系统地揭示不同类型的错误,并且耗费最少时间与最小工作量。
如果成功构造了测试(根据上述目标),则能够在软件中揭示错误。测试的第二个好处在于它证实了软件依据规约所具有的功能及其性能需求,此外,构造测试时的数据收集提供了软件可*性以及软件整体质量的一些信息。但是,有一件事测试无法完成:
测试无法说明错误不存在,它只能表示软件错误已经出现。
在构造测试时必须牢记这一点。
1.1.2测试原则
在设计有效的测试用例之前,软件工程师必须理解软件测试的基本原则。Davie[DAV95]提出了一组①测试原则:
●所有的测试都应追溯到用户需求。正如我们所知,软件测试的目标在于揭示错误。而最严重的错误(从用户角度来看)是那些导致程序无法满足需求的错误。
●应该在测试工作真正开始的前较长时间内就进行测试计划。测试计划可以在需求模型一完成就开始,详细的测试用例定义可以在设计模型被确定后立即开始,因此,所有测试可以在任何代码被产生前进行计划和设计。
● Pareto原则应用于软件测试。简单而言, Pareto原则暗示着测试发现的错误中的80%很可能起源于程序模块中的20%。当然,问题在于如何孤立这些有疑点的模块并进行彻底的测试。
●测试应从“小规模”开始,逐步转向“大规模”。最初的测试通常把焦点放在单个程序模块上,进一步测试的焦点则转向在集成的模块簇中寻找错误,最后在整个系统中寻找错误。
●穷举测试是不可能的。甚至一个大小适度的程序,其路径排列的数量也非常大,因此,在测试中不可能运行路径的每一种组合,然而,充分覆盖程序逻辑,并确保程序设计中使用的所有条件是有可能的。
●为了达到最佳效果,应该由独立的第三方来构造测试。“最佳效果”指最可能发现错误的测试(测试的主要目标)。由于本章中已经介绍过、并将进一步讨论的那些原因,创建系统的软件工程师并不是构造软件测试的最佳人选。
1.1.3可测试性
在理想的情况下,软件工程师在设计计算机程序、系统或产品时应该考虑可测试性,这就使得负责测试的人能够更容易地设计有效的测试用例,但是,什么是“可测试性”呢?
JamesBach②这样描述可测试性:
软件可测试性就是一个计算机程序能够被测试的容易程度。因为测试是如此的困难,因此,需要知道做些什么才能理顺测试过程。有时,程序员愿意去做对测试过程有帮助的事,而一个包括可能的设计点、特性等等的检查表对他们是很有用的。
肯定存在可用于在很多方面测度可测试性的度量,有时,可测试性被用来表示一个特定测试集覆盖产品的充分程度。在军方还用它来表示工具被检验和修复的容易程度。这两种意义都略不同于“软件可测试性”。下面的检查表提供了一组可测试软件的特征:
可操作性。“运行得越好,被测试的效率越高。”
●系统的错误很少(错误加上测试过程中的分析和报告开销)。
●没有阻碍测试执行的错误。
●产品在功能阶段的演化(允许同时的开发和测试)。
可观察性。“你所看见的就是你所测试的。”
●每个输入有唯一的输出。
●系统状态和变量可见,或在运行中可查询。
●过去的系统状态和变量可见,或在运行中可查询(例如:事务日志)。
●所有影响输出的因素都可见。
●容易识别错误输出。
●通过自测机制自动侦测内部错误。
●自动报告内部错误。
●可获取源代码。
可控制性。“对软件的控制越好,测试越能够被自动执行与优化。”
●所有可能的输出都产生于某种输入组合。
●通过某种输入组合,所有的代码都可能被执行。
●测试工程师可直接控制软件和硬件的状态及变量。
●输入和输出格式保持一致且有结构。
●能够便利地对测试进行说明、自动化和再生。
可分解性。“通过控制测试范围,能够更快地分解问题,执行更灵巧的再测试。”
●软件系统由独立模块构成。
●能够独立测试各软件模块。
简单性。“需要测试的内容越少,测试的速度越快。”
●功能简单性(例如:特性集是满足需求所需的最小集合)
●结构简单性(例如:将体系结构模块化以限制错误的繁殖)。
●代码简单性(例如:采用代码标准为检查和维护提供方便)。
稳定性。“改变越少,对测试的破坏越小。
●软件的变化是不经常的。
●软件的变化是可控制的。
●软件的变化不影响已有的测试。
●软件失效后能得到良好恢复。
易理解性。“得到的信息越多,进行的测试越灵巧。”
●设计能够被很好地理解。
●内部、外部和共享构件之间的依赖性能够被很好地理解。
●设计的改变被通知。
●可随时获取技术文档。
●技术文档组织合理。
●技术文档明确详细。
●技术文档精确性稳定。
软件工程师可运用James Bach提出的这些属性来开发可测试的软件配置(即程序、数据和文档)。
但是关于测试本身呢? Kaner, Falk和Nguyen[KAN93]给出了“好”测试的一些属性:
1.一个好的测试发现错误的可能性很高。为了达到这个目标,测试者必须理解软件,并尝试设想软件如何才能失败,理想,被探测的错误类别,例如,在GUI(图形用户界面)中有一种潜在的错误,即错误识别鼠标位置。应该设计一个测试集来验证鼠标位置识别的错误。
2.一个好的测试并不冗余。测试的时间和资源是有限的,没有必要构造一个与其他测试用途完全相同的测试,每一个测试都应该有不同的用途(哪怕是细微的差异)。例如,软件SafeHome①中有一个模块被用来识别用户密码以决定是否启动系统,为了测试密码输入的错误,测试者设计了一系列的输入密码测试。在不同的测试中输入有效与无效密码(四个数字),然而,每一个有效/无效密码将检测一种不同错误模式,例如,一个将8080作为有效密码的系统将不会接受非法密码1234,如果接收1234,将产生错误,另一个测试输入1235,与1234的测试意图相同,因此是冗余的,然而,非法输入8081或8180就有些细微的差异,即对与有效密码相近但并不相同的密码该进行测试。
3.一个好的测试应该是“最佳品种” [KAN93]。
在一组目的相似的测试中,时间和资源的限制可能只影响其某个子集的执行,此时,应该使用最可能找到所有错误的测试。
4.一个好的测试既不会太简单,也不会太复杂。虽然有时会将一组测试组合到一个测试用例中,其副作用可能屏蔽错误,通常,每一个测试应该独立执行。
1.2 测试用例设计
软件和其他工程产品的测试设计与产品本身的设计一样具有挑战性,然而由于已经讨论过的一些原因,软件工程师经常将测试作为一种事后的措施,开发一些“感觉上正确”但是缺乏完整保证的测试用例。再回头看看测试目标,我们必须设计出最可能发现最多数量的错误、并耗费最少时间和最小代价的测试。
在过去的20年,出现了大量的测试用例设计方法,为开发人员进行测试提供了系统的方法。更重要的是,方法提供了一种有助于确保完全测试的机制,并提供了揭示软件错误的最高可能性。
能够采用以下两种方法之一对任何工程化产品(以及大多数其他东西)进行测试:
(1)若了解产品的特定功能,则构造测试,以证实各功能完全可执行,同时在各功能中寻找错误;
(2)若了解产品的内部构造,则构造测试,以确保“所有齿轮吻合”,即内部操作依据规约执行,而且所有的内部构件被充分利用。第一种测试方法被称为黑盒测试,第二种则被称为白盒测试。
如果考虑计算机软件,黑盒测试指在软件界面上进行的测试,虽然设计黑盒测试是为了发现错误,它们却被用来证实软件功能的可操作性;证实能很好地接收输入,并正确地产生输出;以及证实对外部信息完整性(例如:数据文件)的保持。黑盒测试检验系统的一些基本特征,很少涉及软件的内部逻辑结构。
软件的白盒测试依赖对程序细节的严密检验,提供运用特定条件和/与循环集的测试用例,对软件的逻辑路径进行测试,在不同的点检验“程序的状态”以判定预期状态或待验证状态与真实状态是否相符。
一眼看去,可能认为全面的白盒测试将产生“百分之百正确的程序”,需要我们做的只是定义所有的逻辑路径、开发相应的测试用例,并评估结果,简而言之,详尽地生成用例以测试程序逻辑。不幸的是,穷举测试带来了必然的计算问题,即使是很小的程序,可能的逻辑路径数量也非常大,例如,考虑 100行C语言程序,在一些基本的数据声明之后,程序包含两个嵌套循环,根据输入的条件分别执行1到20次,在内部循环中,需要四个if-then-else结构,该程序中大约有1014条可能路径!
为了正确表达这个数值,我们假设开发了一个有魔力的测试处理器(“有魔力”是因为不存在这样的处理器)进行穷举测试。该处理器能在一毫秒内开发一个测试用例、进行运行并评估结果,如果每天运行24小时,每年运行365天,则需要3170年的时间来测试这个程序。不可否认,这将导致大多数开发进度表的混乱,对大型软件系统不可能进行穷举测试。
然而,白盒测试不应该被抛弃,可选择有限数量的重要逻辑路径进行测试,检测重要数据结构的有效性,可以综合黑盒测试和白盒测试的属性提供一种方法,以验证软件界面,并有选择地保证软件内部工作的正确性。
1.3白盒测试
白盒测试,有时称为玻璃盒测试,是一种测试用例设计方法,它使用程序设计的控制结构导出测试用例。使用白盒测试方法,软件工程师能够产生测试用例
(1)保证一个模块中的所有独立路径至少被使用一次;
(2)对所有逻辑值均需测试true和 false;
(3)在上下边界及可操作范围内运行所有循环;
(4)检查内部数据结构以确保其有效性。
此时可能会提出一个合理的问题:“我们应该更注重于保证程序需求的实现,为什么要花费时间和精力来担心(和测试)逻辑细节?”换一种说法,我们为什么不将所有精力用于黑盒测试呢?答案在于软件自身的缺陷
●逻辑错误和不正确假设与一条程序路径被运行的可能性成反比。当我们设计和实现主流之外的功能、条件或控制时,错误往往开始出现在我们的工作中。日常处理往往被很好地了解(和很好地细查),而“特殊情况”的处理则难于发现。
●我们经常相信某逻辑路径不可能被执行,而事实上,它可能在正常的基础上被执行。程序的逻辑流有时是违反直觉的,这意味着我们关于控制流和数据流的一些无意识的假设可能导致设计错误,只有路径测试才能发现这些错误。
●印刷上的错误是随机的。当一个程序被翻译为程序设计语言源代码时,有可能产生某些打印错误,很多将被语法检查机制发现,但是,其他的会在测试开始时才会被发现。打印错误出现在主流上和不明显的逻辑路径上的可能性是一样的。
上述任何一条原因都是该进行白盒测试的论据,黑盒测试,不管它多么全面,都可能忽略前面提到的某些类型的错误。正如Beizer所说[BEI90]:“错误潜伏在角落里,聚集在边界上”。白盒测试更可能发现它们。
1.4基本路径测试
基本路径测试是Tom McCabe[MCC76]首先提出的一种白盒测试技术,基本路径测试方法上”。允许测试用例设计者导出一个过程设计的逻辑复杂性测度,并使用该测度作为指南来定义执行路径的基本集。从该基本集导出的测试用例保证对程序中的每一条语句至少执行一次。
1.4.1流图符号
在介绍基本路径方法之前,必须先介绍一种简单的控制流表示方法,即流图或程序图①。流图使用图1-1中的符号描述逻辑控制流,每一种结构化构成元素有一个相应的流图符号。
为了说明流图的用法,我们采用图1-2中的过程设计表示法,此处,流程图用来描述程序控制结构。图 1-2b将流程图映射到一个相应的流图(假设流程图的菱形决定框中不包含复合条件)。在图1-2b中,每一个圆,称为流图的节点,代表一个或多个语句。一个处理方框序列和一个菱形决测框可被映射为一个节点,流图中的箭头,称为边或连接,代表控制流,类似于流程图中的箭头。一条边必须终止于一个节点,即使该节点并不代表任何语句(例如:参见if-else-then结构的符号)。由边和节点限定的范围称为区域。计算区域时应包括图外部的范围①。
任何过程设计表示法都可被翻译成流图,图 1-3显示了一个程序设计语言(PDL,ProgramDesign Language)片段及其对应的流图,注意,对PDL语句进行了编号,并将相应的编号用于流图中。
程序设计中遇到复合条件时,生成的流图变得更为复杂。当条件语句中用到一个或多个布尔运算符(逻辑OR,AND,NAND,NOR)时,就出现了复合条件。图1-4中,将一个PDL片段翻译为流图,注意,为语句IF a OR b中的每一个a和b创建了一个独立的节点,包含条件的节点被称为判定节点,从每一个判定节点发出两条或多条边。
1.4.2环形复杂性
环形复杂性是一种为程序逻辑复杂性提供定量测度的软件度量,将该度量用于基本路径方法,计算所得的值定义了程序基本集的独立路径数量,并为我们提供了确保所有语句至少执行一次的测试数量的上界。
独立路径是指程序中至少引进一个新的处理语句集合或一个新条件的任一路径。采用流图的术语,即独立路径必须至少包含一条在定义路径之前不曾用到的边。例如,图1-2b中所示流图的一个独立路径集合为:
路径1:1-11
路径2:1-2-3-4-5-10-1-11
路径3:1-2-3-6-8-9-10-1-11
路径4:1-2-3-6-7-9-10-1-11
注意,每一条新的路径都包含了一条新边。路径1-2-3-4-5-10-1-2-3-6-8-9-10-1-11不是独立路径,意味它只是已有路径的简单合并,并未包含任何新边。
上面定义的路径1,2,3和4包含了图 1-2b所示流图的一个基本集,简而言之,如果能将测试设计为强迫运行这些路径(基本集),那么程序中的每一条语句将至少被执行一次,每一个条件执行时都将分别取true和false。应该注意到基本集并不唯一,实际上,给定的过程设计可派生出任意数量的不同基本集。
如何才能知道需要寻找多少条路径呢?对环形复杂性的计算提供了这个问题的答案。环形复杂性以图论为基础,为我们提供了非常有用的软件度量。可用如下三种方法之一来计算复杂性:
1.流图中区域的数量对应于环形的复杂性。
2.给定流图G的环形复杂性——V(G),定义为V(G)=E-N+2,E是流图中边的数量,N是流图节点数量。
3.给定流图G的环形复杂性——V(G),也可定义为V(G)=P+1,P是流图G中判定节点的数量。
再回到图1-2b。可采用上述任意一种算法来计算环形复杂性。
1.流图有4个区域。
2.V(G)=11条边-9个节点+2=4。
3.V(G)=3个判定节点+1=4。
因此,图1-2b的环形复杂性是4。
更重要的是,V(G)的值提供了组成基本集的独立路径的上界,并由此得出覆盖所有程序语句所需的测试设计数量的上界。
1.4.3导出测试用例
基本路径测试方法可用于过程设计或源代码生产。本节中,我们将基本路径测试表示为一系列步骤,图1-5中PDL所描述的过程“求平均值”将被用于阐明测试用例设计方法中的各个步骤。注意,“求平均值”虽然是一个非常简单的算法,但是仍然包含了复合条件和循环。
1.以设计或代码为基础,画出相应的流图。
2.确定结果流图的环形复杂性。可采用上一节中的任意一种算法来计算环形复杂性——V(G)。应该注意到,计算V(G)并不一定要画出流图,计算PDL中的所有条件语句数量(过程求平均值中复合条件语句计数为2),然后加1即可得到环形复杂性。在图1-6中,
V(G)=6个区域
V(G)=18条边-14个节点+2=6。
V(G)=5个判定节点+1=6。
3.确定线性独立的路径的一个基本集。V(G)的值提供了程序控制结构中线性独立的路径的数量,在过程求平均值中,我们指定六条路径:
路径1:1-2-10-11-13
路径2:1-2-10-12-13
路径3:1-2-3-10-11-13
路径4:1-2-3-4-5-8-9-2-……
路径5:1-2-3-4-5-6-8-9-2-……
路径6:1-2-3-4-5-6-7-8-9-2-……
路径4、5和6后面的省略号(……)表示可加上控制结构其余部分的任意路径。通常在导出测试用例时,识别判定节点是很有必要的。本例中,节点2、3、5、6和10是判定节点。
4.准备测试用例,强制执行基本集中每条路径。测试人员可选择数据以便在测试每条路径时适当设置判定节点的条件。满足上述基本集的测试用例如下:
路径1测试用例:
value(k)=有效输入,其中k<i
value(i)=-999,其中2≤i≤100
期望结果:基于k的正确平均值和总数
注意:路径1无法独立测试,必须作为路径4、5和6测试的一部分。
路径2测试用例:
value(1)=-999
期望结果:求平均值=-999;其他按初值汇总
路径3测试用例:
试图处理101或更大的值
前100个数值应该有效
期望结果:与测试用例1相同
路径4测试用例:
value(i)=有效输入,其中i<100
value(k)<最小值,其中k<i
期望结果:基于k的正确平均值和总数
路径5测试用例:
value(i)=有效输入,其中i<100
value(k)>最大值,其中k≥i
期望结果:基于k的正确平均值和总数
路径6测试用例:
value(i)=有效输入,其中i<100
期望结果:基于k的正确平均值和总数
执行每个测试用例,并和期望值比较,一旦完成所有测试用例,测试者可以确定在程序中的所有语句至少被执行一次。
重要的是要注意,某些独立路径(如,例子中的路径1)不能以独立的方式被测试,即,穿越路径所需的数据组合不能形成程序的正常流,在这种情况下,这些路径必须作为另一个路径测试的一部分来进行测试。
1.4.4 图矩阵
导出流图和决定基本测试路径的过程均需要机械化,为了开发辅助基本路径测试的软件工具,称为图矩阵(graph matrix)的数据结构很有用。
图矩阵是一个正方形矩阵,其大小(即列数和行数)等于流图的节点数。每列和每行都对应于标识的节点,矩阵项对应于节点间的连接(边),图1-7显示了一个简单的流图及其对应的图矩阵[BEI90]。
该图中,流图的节点以数字标识,边以字母标识,矩阵中的字母项对应于节点间的连接,例如,边b连接节点3和节点4。
这里,图矩阵只是流图的表格表示,然而,对每个矩阵项加入连接权值(link weight),图矩阵就可以用于在测试中评估程序的控制结构,连接权值为控制流提供了另外的信息。最简单情况下,连接权值是 1(存在连接)或0(不存在连接),但是,连接权值可以赋予更有趣的属性:
●执行连接(边)的概率。
●穿越连接的处理时间。
●穿越连接时所需的内存。
●穿越连接时所需的资源。
举例来说,我们用最简单的权值(0或1)来标识连接,图1-7所示的图矩阵重画为图1-8。字母替换为1,表示存在边(为清晰起见,没有画出0),这种形式的图矩阵称为连接矩阵(linkmatrix)。图1-8中,含两个或两个以上项的行表示判定节点,所以,右边所示的算术计算就提供了另一种环形复杂性计算(参1.4.2节)的方法。
Beizer[BEI90]提供了可用于图矩阵的其他数学算法的处理,利用这些技术,设计测试用例的分析就可以自动化或部分自动化。
1.5 控制结构测试
1.4节所述的基本路径测试技术是控制结构测试技术之一。尽管基本路径测试简单高效,但是,其本身并不充分。本节讨论控制结构测试的其他变种,这些测试覆盖并提高了白盒测试的质量。
1.5.1 条件测试
条件测试是检查程序模块中所包含逻辑条件的测试用例设计方法。一个简单条件是一个布尔变量或一个可能带有NOT(“┓”)操作符的关系表达式。关系表达式的形式如:
E1<关系操作符>E2
其中E1和E2是算术表达式,而<关系操作符>是下列之一:“<”,“≤”,“=”,“≠”(“┓=”),“>”,或“≥”。复杂条件由简单条件、布尔操作符和括弧组成。我们假定可用于复杂条件的布尔算子包括OR“|”,AND“&”和NOT“┓”,不含关系表达式的条件称为布尔表达式。
所以条件的成分类型包括布尔操作符、布尔变量、布尔括弧(括住简单或复杂条件)、关系操作符或算术表达式。
如果条件不正确,则至少有一个条件成分不正确,这样,条件的错误类型如下:
●布尔操作符错误(遗漏布尔操作符,布尔操作符多余或布尔操作符不正确)。
●布尔变量错误。
●布尔括弧错误。
●关系操作符错误。
●算术表达式错误。
条件测试方法注重于测试程序中的条件。本节后面讨论的条件测试策略主要有两个优点,首先,测度条件测试的覆盖率是简单的,其次,程序的条件测试覆盖率为产生另外的程序测试提供了指导。
条件测试的目的是测试程序条件的错误和程序的其他错误。如果程序P的测试集能够有效地检测P中的条件错误,则该测试集可能也会有效地检测P中的其他错误,此外,如果测试策略对检测条件错误有效,则它也可能有效地检测程序错误。
已经提出了几个条件测试策略。分支测试可能是最简单的条件测试策略,对于复合条件C,C的真分支和假分支以及C中的每个简单条件都需要至少执行一次[MYE97]。
域测试(Domain testing)[WHI80]要求从有理表达式中导出三个或四个测试,有理表达式的形式如:
E1<关系操作符>E2
需要三个测试分别用于计算E1的值是大于、等于或小于E2的值[HOW82]。如果<关系操作符>错误,而E1和E2正确,则这三个测试能够发现关系算子的错误。为了发现E1和E2的错误,计算E1小于或大于E2的测试应使两个值间的差别尽可能小。
有n个变量的布尔表达式需要2n个可能的测试(n>0)。这种策略可以发现布尔操作符、变量和括弧的错误,但是只有在n很小时实用。
也可以派生出敏感布尔表达式错误的测试[FOS84,TAI87]。对于有n个布尔变量(n>0)的单布尔表达式(每个布尔变量只出现一次),可以很容易地产生测试数小于2n的测试集,该测试集能够发现多个布尔操作符错误和其他错误。
Tai[TAI89]建议在上述技术之上建立条件测试策略,称为BRO(branch and relational试集operator)的测试保证能发现布尔变量和关系操作符只出现一次而且没有公共变量的条件中的分支和条件操作符错误。
BRO策略利用条件C的条件约束。有n个简单条件的条件C的条件约束定义为(D1,D2,…,Dn),其中Di(0<i≤n)表示条件C中第i个简单条件的输出约束。如果C的执行过程中C的每个简单条件的输出都满足D中对应的约束,则称条件C的条件约束D由C的执行所覆盖。
对于布尔变量B,B输出的约束说明B必须是真(t)或假(f)。类似地,对于关系表达式,符号<、=、>用于指定表达式输出的约束。
作为简单的例子,考虑条件
C1∶B1&B2
其中B1和B2是布尔变量。C1的条件约束式如(D1,D2),其中D1和D2是“t”或“f”,值(t,f)是C1的条件约束,由使B1为真B2为假的测试所覆盖。BRO测试策略要求约束集{(t,t),(f,t),(t,f)}由C1的执行所覆盖,如果C1由于布尔算子的错误而不正确,至少有一个约束强制C1失败。
作为第二个例子,考虑
C2∶B1&(E3=E4)
其中B1是布尔表达式,而E3和E4是算术表达式。C2的条件约束形式如(D1,D2),其中D1是“t”或“f”,D2是<,=或>。除了C2的第二个简单条件是关系表达式以外,C2和C1相同,所以可以修改C1的约束集{(t,t),(f,t),(t,f)},得到C2的约束集,注意(E3=E4)的“t”意味着“=”,而(E3=E4)的“f”意味着“>”或“<”。分别用(t,=)和(f,=)替换(t,t)和(f,t),并用(t,<\<>)和(t,>)替换(t,f),就得到C2的约束集{(t,=),(f,=),(t,<),(t,>)}。上述条件约束集的覆盖率将保证检测C2的布尔和关系算子的错误。
作为第三个例子,考虑
C3∶(E1>E2)&(E3=E4)
其中E1、E2、E3和E4是算术表达式。C3的条件约束形式如(D1,D2),其中D1和D2是<、=或>。除了C3的第一个简单条件是关系表达式以外,C3和C2相同,所以可以修改C2的约束集得到C3的约束集,结果为
{(>,=),(=,=),(<,=),(>,>),(>,<)}
上述条件约束集能够保证检测C3的关系操作符的错误。
1.5.2 数据流测试
数据流测试方法按照程序中的变量定义和使用的位置来选择程序的测试路径。已经有不少关于数据流测试策略的研究。
为了说明数据流测试方法,假设程序的每条语句都赋予了独特的语句号,而且每个函数都不改变其参数和全局变量。对于语句号为S的语句,
DEF(S)={X|语句S包含X的定义}
USE(S)={X|语句S包含X的使用}
如果语句S是if或循环语句,它的DEF集为空,而USE集取决于S的条件。如果存在从S到S′的路径,并且该路径不含X的其他定义,则称变量X在语句S处的定义在语句S′仍有效。
变量X的定义—使用链(或称DU链)形式如[X,S,S′],其中S和S′是语句号,X在DEF(S)和USE(S′)中,而且语句S定义的X在语句S′有效。
一种简单的数据流测试策略是要求覆盖每个DU链至少一次。我们将这种策略称为DU测试策略。已经证明DU测试并不能保证覆盖程序的所有分支,但是,DU测试不覆盖某个分支仅仅在于如下之类的情况:if-then-else中的then没有定义变量,而且不存在else部分。这种情况下,if语句的else分支并不需要由DU测试覆盖。
数据流测试策略可用于为包含嵌套if和循环语句的程序选择测试路径,为此,考虑使用DU测试为如下的PDL选择测试路径:
proc x
B1;
do while C1
if C2
then
if C4
then B4;
else B5;
endif;
else
if C3
then B2;
else B3;
endif;
endif;
enddo;
B6;
end proc;
为了用DU测试选择控制流图的测试路径,需要知道PDL条件或块中的变量定义和使用。假设变量X定义在块B1,B2,B3,B4和B5的最后一条语句之中,并在块B2,B3,B4,B5和B6的第一条语句中使用。DU测试策略要求执行从每个B(0<i≤5)到Bj(0<j≤6)的最短路径(这样的测试也覆盖了条件C1,C2,C3和C4中的变量使用)。尽管有25条X的DU链,只需5条路径覆盖这些DU链。原因在于可用5条从Bi(0<i≤5)到B6的路径覆盖X的链,而这5条链包含循环的迭代就可以覆盖其他的DU链。
注意如果要用分支测试策略为上述的PDL选择测试路径,并不需要另外的信息。为了选择BRO测试的路径,只需知道每个条件和块的结构。(选择程序的路径之后,需要决定该路径是否实用于该程序,即是否存在执行该路径的至少一个输入)。
由于变量的定义和使用,程序中的语句都彼此相关,所以数据流测试方法能够有效地发现错误,但是,数据流测试的覆盖率测度和路径选择比条件测试更为困难。
1.5.3循环测试
循环是大多数软件实现算法的重要部分,但是,在软件测试时却很少注意它们。
循环测试是一种白盒测试技术,注重于循环构造的有效性。有四种循环[BEI90]:简单循环,串接循环,嵌套循环和不规则循环(如图1-9所示)。
简单循环。下列测试集应当用于简单循环,其中n是允许通过循环的最大次数。
1.整个跳过循环。
2.只有一次通过循环。
3.两次通过循环。
4.m次通过循环,其中m<n。
5.n-1,n,n+1次通过循环。
嵌套循环。如果要将简单循环的测试方法用于嵌套循环,可能的测试数就会随嵌套层数成几何级增加,这会导致不实际的测试数目,Beizer[BEI90]提出了一种减少测试数的方法:
1.从最内层循环开始,将其他循环设置为最小值。
2.对最内层循环使用简单循环测试,而使外层循环的迭代参数(即循环计数)最小,并为范围外或排除的值增加其他测试。
3.由内向外构造下一个循环的测试,但其他的外层循环为最小值,并使其他的嵌套循环为“典型”值。
4.继续直到测试完所有的循环。
串接循环。如果串接循环的循环都彼此独立,可以使用嵌套循环的策略测试串接循环。但是,如果两个循环串接起来,而第一个循环的循环计数是第二个循环的初始值,则这两个循环并不是独立的。如果循环不独立,则推荐使用嵌套循环的方法进行测试。
不规则循环。尽可能的情况下,要将这类循环重新设计为结构化的程序结构。
1.6黑盒测试
黑盒测试注重于测试软件的功能性需求,也即黑盒测试使软件工程师派生出执行程序所有功能需求的输入条件。黑盒测试并不是白盒测试的替代品,而是用于辅助白盒测试发现其他类型的错误。
黑盒测试试图发现以下类型的错误:
(1)功能不对或遗漏,
(2)界面错误,
(3)数据结构或外部数据库访问错误,
(4)性能错误和
(5)初始化和终止错误。
白盒测试在测试的早期执行,而黑盒测试主要用于测试的后期。黑盒测试故意不考虑控制结构,而是注意信息域。测试用于回答以下问题:
●如何测试功能的有效性?