1.2 静态分析
静态分析也不要求实际运行被测试对象,通常和软件代码或者系统架构有关,它是静态测试的重要组成部分。静态分析通常通过工具支持来完成,如拼写检查工具可以认为是静态分析工具的一种,因为它可以发现测试对象文档中的拼写错误。静态分析指的就是这种类型的检查,它并没有运行测试对象。
静态分析与评审紧密联系,如果在评审之前进行了静态分析,并发现了测试对象的一些缺陷,那么可以减少评审过程中需要检查的地方。由于静态分析通常由工具支持,因此工作量会比评审少很多。例如,测试对象的文档有严格规则,通过工具支持的静态分析可以有效地发现其中的缺陷和不一致性,从而提高文档的质量。
静态测试(本节主要指静态分析)是动态测试的重要补充,可以发现测试对象中的不一致和可能存在问题的区域。例如,测试对象和编程标准的差异,以及禁止使用的易产生错误的程序结构等类型的缺陷。静态分析经常发现的缺陷类型如下。
(1)违背语法规则。
(2)违背编程规范和标准。
(3)控制流异常。
(4)数据流异常。
在组件测试或集成测试过程中,开发人员通常会使用一些静态分析工具检查被测对象是否满足编程指南或编程规范。静态分析的主要优点如下。
(1)在测试执行之前尽早发现测试对象中的缺陷和问题。
(2)通过度量计算(如高复杂性测量),相关人员在早期就可以对可能存有问题的代码和设计(高风险区域)保持相应的警惕。
(3)发现在动态测试过程中不容易发现的一些缺陷和异常。
(4)发现软件模块之间的相互关联的不一致性。
(5)可以改进代码和设计,以增强可维护性。
开发人员通常在组件测试和集成测试之前或期间使用静态分析工具(如检查预先定义的规则或编程规范),而设计人员在软件建模期间也会使用静态分析工具。
静态分析工具通常会产生大量的告警和注释信息。为了更加有效地使用静态分析工具,产生的大量信息必须进行适当的处理,如通过设置工具参数按照一定的顺序或规则控制产生的信息;否则使用这种工具的效率将会大大打折。编译器也可以为静态分析提供一些帮助,包括度量的计算等。
静态分析的一个目的是发现测试对象(如软件代码或者系统架构)中的缺陷或者可能存在缺陷的地方;另一个重要目的是得到测试对象的度量数据,从而对测试对象的质量进行评估。在本节中,静态分析根据其对象的不同分为代码和架构的静态分析。
1.2.1 基于代码的分析
基于代码的静态分析的测试对象是软件代码,它可以测试或检查任何类型的软件代码,而不需要运行代码。基于代码的静态分析有多种不同的技术,本节主要讨论控制流分析、数据流分析、编码标准一致性和生成代码度量。
(1)控制流分析。控制流指的是组件或者系统中的一系列顺序发生的事件或路径,测试对象的控制流一般通过控制流图直观地表示。控制流分析基于控制流图中的事件或者路径开展,可以提供测试对象的逻辑判定点和其结构复杂度的信息,测试对象的代码控制流同时也是白盒测试(基于结构的测试)技术的基础。
程序执行路径中发生变化处表示为分支,分支可能是判定语句或循环语句。例如,判定语句IF的判定结果为真或假会引导程序执行不同的路径。通过测试对象的控制流图的清楚描述,程序结构顺序更加容易理解;同时可以发现其中的一些异常,如执行语句异常跳出循环体。尽管这些异常并不一定会导致失效,但是它们可能不符合结构化编程的原则。如果控制流图中的事件顺序和相互关系很难理解,则需要修正程序的内容,因为复杂的语句结构常常意味着潜在的错误风险。控制流图一般不通过手工方式生成,而需要相关工具支持。
控制流分析是代码静态分析中的重要手段,它经常可以发现以下缺陷或者异常。
● 测试对象代码中包含多个分支路径,如switch语句的索引变量的数值是否会和可能的分支数量不同?在下面代码中,num的取值是不是总是1、2、3、4或5?如果num的取值超过了代码中的几个索引,测试对象代码如何处理?
while {1} switch -exact -- $num { "1" { function 1 continue} "2" { function 2 continue } "3" { function 3 continue } "4" { function 4 continue } "5" { function 5 continue } } }
● 测试对象代码中的所有循环是否都能终止?程序、模块或者子程序是否最终都能终止?仍以上面这段代码为例,如果其索引的数量和分支的数量相同,但是由于每个分支最终都是以continue为结束,那么整个while语句将无法正常终止。
● 如果测试对象程序的循环入口条件没有满足,其中的循环体是否可能从来没有执行过,从而成为“死代码”。例如,在下面的代码中,如果初始条件j的取值比k的取值大,或者m的初始取值为假,情况会如何?
for {set i $j} {$i < $k} {incr i} { s1 s2 while {$m} { s3 } }
● 测试对象代码中是否存在“仅差一个”这样的错误,如循环数量多一次或者少一次。这在以0开始的循环语句中经常出现,因为常常忘记将0作为一次计数。例如,代码需要定义具有10 个元素的数组。下面的语句存在错误,因为它实际上定义了11个元素的数组:
array set arr "" for {set i 0} {$i <= 10 } {incr i} { set arr($i) $i } parray arr
● 测试对象代码中存在语句组或者代码块的概念,如if-else语句是否相互对应?语句组或者代码块中的左括号和右括号是否相互对应?
● 是否存在不能穷尽的判断?例如,如果一个输入参数的预期值是1、2或者3,当参数值不为1和2时是否在逻辑上假设了参数值必定为3?如果是,这种假设在程序实现中是否有效?
(2)数据流分析。数据流指的是数据对象的顺序和可能状态变换的抽象表示,对象的状态可以是创建(Creation/Defined)、使用(Usage/Used)和销毁(Destruction/Killed)。数据流分析是一种基于变量定义和使用的静态分析模式,它在检测由于编码错误导致的变量赋值错误方面是一种非常有用的技术。
数据流分析用来测试变量设置点和使用点之间的路径,这些路径有时也称为“定义-使用对”(definition-use或者du-pairs)或“设置-使用对”。通过数据流分析生成的测试集用来获得针对每个变量的“定义-使用对”的100%覆盖(在可能的情况下)。这个技术尽管称为“数据流分析”,但是当它追踪设置和使用每个变量时也可能需要贯穿软件代码的控制流,所以也需要考虑测试对象代码的控制流。
包含数据值的变量有不同的状态,从创建、使用到销毁是一个完整的过程。在有的编程语言,如FORTRAN和BASIC中,变量的创建和销毁是自动的。在第1次赋值变量时创建,而在程序退出时销毁;有的编程语言,如C和C++等,变量在使用之前必须首先声明。在执行到定义变量的语句时,程序创建这些变量。在程序块结束时,这些变量自动销毁,这就是一个变量的使用范围。变量既可以使用在单独的计算语句中,也可以作为条件判断的组成部分。无论哪种情况,在变量使用之前都需要为其赋值。
在程序代码路径中首次出现的变量可能存在的状态组合如下。
● ~d:变量不存在或者没有定义(通过~表示),然后定义(d)。
● ~u:变量不存在或者没有定义,然后使用(u)。
● ~k:变量不存在或者没有定义,然后销毁(k)。
其中第1种情况~d是正确的;而第2种~u是错误的,因为变量在使用之前必须定义;第3种~k可能是错误的,因为在创建变量之前销毁变量可能是一个潜在的编程错误。
d、u和k的具体含义如下。
● d:变量声明或者定义,并且变量已经赋值。
● u:读取及使用。
● k:声明或定义了变量,但还没有为其赋值,或已经释放变量(模块或函数结束时)。
针对程序代码路径中的变量执行顺序,变量的状态d、u和k组合可能有如下9种情况。
● dd:变量赋值之后再次赋值,可疑的,可能是编程错误。
● du:变量赋值之后使用,正确的,是程序代码中的正常情况。
● dk:变量定义之后销毁,可疑的,可能是编程错误。
● ud:变量使用之后再定义,可以接受。
● uu:变量使用之后再使用,可以接受。
● uk:变量使用之后销毁,可以接受。
● kd:变量销毁之后再定义,可以接受,变量销毁之后重新定义。
● ku:变量未定义或销毁之后使用,严重的问题,在变量不存在或者没有定义的情况下,使用变量是一个错误。
● kk:变量未定义或销毁之后再销毁,可能是编程错误。
数据流分析可以基于数据流图展开。数据流图类似于控制流图,描述了测试对象代码的处理过程;同时详细描述了代码中变量的创建、使用和销毁的状态。通过检查数据流图,验证测试对象代码中每个变量的状态组合是否正确。
图1-2所示为一个数据流图的例子。
图1-2 一个数据流图的例子
通过分析其中每个变量的执行顺序状态,可以发现存在的一些问题。
首先分析变量x,存在的变量状态组合如下。
● ~d:正确的情况。
● dd:定义之后再定义,可能是一个编程错误。
● du:定义之后使用,正确的情况。
其次分析变量y,其状态的可能组合如下。
● ~u:定义变量之前使用变量,严重的错误。
● ud:变量使用之后再定义,可以接受。
● du:变量定义之后使用,正确的情况。
● uk:变量使用之后销毁,可以接受。
● dk:变量定义之后销毁,可能是编程错误。
最后分析变量z,可能的状态组合如下。
● ~k:定义变量之前销毁变量,可能是编程错误。
● ku:变量销毁之后使用变量,严重的问题。
● uu:使用变量之后再使用变量,正确的情况。
● ud:变量使用之后再定义,可以接受。
● kk:销毁变量之后再销毁,可能是编程错误。
● kd:变量销毁之后再定义,可以接受。
● du:变量定义之后使用变量,正常情况。
因此在这个数据流分析的例子中,存在如下问题。
● 变量x存在dd的编程错误。
● 变量y存在~u和dk的编程错误。
● 变量z存在~k、ku和kk编程错误。
(3)编码标准的一致性。编码标准是开发人员编写代码的指导性规范,在静态分析过程中可以根据遵循的编码标准对测试对象进行一致性评估。编码标准包括架构和编程结构的标准,规范的编码标准有助于软件的维护和测试,特定的编程语言要求能通过静态分析的编码标准一致性检查。
许多静态分析工具可以检测出软件代码中存在的违背编码标准的问题,如检查IF语句中是否没有进行合理的缩排。而有些更加复杂的静态分析工具可能允许定义特定的编码标准,从而按照定义的标准检查测试对象代码。
经验表明,遵循编码规范具有以下优点。
● 编码标准的强制性和一致性可以确保开发人员按照编码标准的要求编写代码,从而减少代码中的缺陷。
● 良好的编码架构有利于采用基于结构的技术来创建测试用例,从而易于组件测试。
● 良好的规范和架构有利于其他人员更容易地接受代码,在代码规范和风格上面更加趋向一致,从而易于维护。
● 新人可以更快地适应编码环境。
例1-3 C++编码标准命名规则(方法和函数命名)
通常每个方法和函数都是执行一个动作,所以命名应该清楚地说明其功能。例如,用CheckForErrors()代替ErrorCheck(),用DumpDataToFile()代替DataFile(),这样也可以将功能函数和数据分开。
有时如下后缀名是有用的。
● Max:含义为某实体所能赋予的最大值。
● Cnt:一个运行中的计数变量的当前值。
● Key:键值。
例如,RetryMax表示最多重试次数,RetryCnt表示当前重试次数。
有时如下前缀名是有用的。
● Is:含义为问一个关于某种事物的问题,无论何时,当人们看到Is就会知道这是一个问题。
● Get:含义为取得一个数值。
● Set:含义为设定一个数值
例如,IsHitRetryLimit。
(4)生成代码度量。静态分析工具可以提供多种代码度量数据,静态测试过程中产生的这些代码度量有助于提高代码的可维护性或可靠性。常见代码度量包括圈复杂度(Cyclomatic Complexity)、规模(Size)、注释率(Comment Frequency)、嵌套的层数(Number of Nested Levels)和函数调用数(Number of Function Calls)。
测试对象的质量特性可以通过度量数据度量,在测试过程中需要检查度量值,以确定它们是否满足特定的需求,如ISO 9126定义的质量特性。定义软件质量特性度量的目的是获取软件质量的抽象测量模型,通常只表达测试对象某个方面的特点,因此不同的度量指标结合使用才有意义。
圈复杂度是常见的复杂度之一,它是由Thomas McCabe在1976年创建的,所以也称为“McCabe复杂度”,更多关于圈复杂度的内容参见2.8节。
1.2.2 基于架构的分析
静态分析的对象不仅可以是测试对象的代码,也可以扩展到如下产品架构。
(1)产品或者系统中的组件之间的相互调用关系。
(2)产品图形化用户接口的菜单结构。
(3)网络产品的网页或者其他功能结构。
本节在基于架构的静态分析中主要针对网站和调用图两个方面讨论。
(1)网站。网站是不同网页组成的层次化结构,网页由不同元素组成,如表格、文字、图片和链接等。网站的结构通过HTML格式描述。许多工具可以分析HTML代码和结构,并且得到详细的网站组成。实施网站分析的人员可以是开发分析人员、网站设计人员和测试人员,以及其他负责维护网站的人员等。分析得到的一个重要结果是网站的结构图,它可以清楚地显示网站的树形结构或者层次结构,从而得到网站结构的深度、复杂度和结构的平衡性。使用静态分析工具也能评价网站的架构,目的是检查网站的树状结构是否平衡。如果不平衡,将导致测试任务更加困难、增加维护的工作量和用户导航困难。
网站的结构图中得到的信息可以用于如下方面。
● 验证网站相关的需求是否已经实现,如网站的深度和网站内容的平衡性,判断网站结构是否需要重新构建。
● 根据网站的结构图估算测试工作量。
● 评估网站可用性,如通过任何一个网页都可以返回上一级页面并逐级返回直到主页。
● 评估网站可维护性,如网站中网页内容是否容易扩展。
网站通常是一个动态产品,因此仅仅基于静态分析是不够的,还需要采用动态分析以发现不同的缺陷和问题。例如,网页中的链接无效、在下载网页中的文件时出现不完整的下载和不稳定的性能。
通过对网站进行架构分析,还可以得到其他方面的信息,如用户的访问模式、网站的导航模式、网站的典型性能和网页显示时间。
有些特定的测试工具包含网络蜘蛛引擎,通过静态分析能提供有关网页大小和下载所需的时间,以及网页是否存在(即http 404错误)等信息,从而可以为开发人员、网站管理员和测试人员提供有用的信息。
(2)调用图。控制流也可以通过其他图表示,最常用的是调用图(Call Graphs),其图中的节点代表程序(如函数和程序块等);边代表调用的关系。
绘制函数调用图对于理解大型程序很有帮助,函数调用图是有向图。如果代码中没有直接或者间接的递归关系,调用图就是无环图。人工方式绘制调用图很困难且易出错,因此绘制调用图一般需要工具支持。
调用图的分析可以分为静态和动态两种,静态分析指的是不运行程序的情况下分析;动态分析需要记录程序实际运行时的函数调用情况。
静态分析源代码获取的调用图的质量取决于分析工具对编程语言的理解程度,如能不能找出正确的C++重载函数。对编程语言理解程度最好的是相关语言的编译器,因此通过为相应编译器增加调用图的功能是实现调用图的有效方法。
动态分析在程序运行时记录函数的调用,然后整理为调用图。与静态分析相比,它能够获得更多的信息,如函数调用的先后顺序和次数。但是动态分析也存在缺点,如果程序中的某些分支没有执行,那么无法记录这些分支的调用函数。
详细分析静态分析得到的调用图信息之后,测试人员可以将精力放在经常或者重复调用的模块或函数上。如果这些模块或者函数比较重要,如安全关键系统中的某些模块,那么它们就是首选的详细和广泛分析的对象。
从测试的角度看,构建程序调用图主要用于以下目的。
● 设计相应测试用例来调用特定的模块或者函数。
● 在代码中设置相关的断点或者位置,确定模块的调用处。
● 为选择集成测试的集成策略提供建议。
● 评估所有代码的结构及其架构的易用性和可维护性。