第十一章 - 软件测试

  • 重点在软件度量和测试方法上

测试技术(分类、 策略)

软件测试定义

  • 使用人工或自动的手段来运行或测定某个软件系统的过程,其目的在于检验它是否满足规定的需求或弄清预期结果与实际结果之间的差别。
  • 软件测试通常被认为只是开发后期的一次性活动,但实际上,应该与整个开发流程紧密地融为一体。敏捷思想的普及使软件测试得到了前所未有的重视,它也是项目成功必不可少的基石。
  • 测试用例(Test Case)是为某个特殊的测试目标而编制的一组测试输入、执行条件及预期结果,以便测试某个程序路径或核实软件是否满足某个特定需求。
    • 测试用例其实就是测试的具体内容,也是软件测试设计的主要工作。

测试分类和测试V模型

  • 在类测试(单元测试)阶段对每个单一的新开发的功能模块进行测试,并且多由开发者本人按照质量保证相关规范进行执行。
  • 集成测试对经过单元测试的类逐步进行集成,构成最终的包或者系统的过程。重点关注每个类与其它相关类的交互过程
  • 系统测试将经过集成测试的构件或系统需要与其它通过预定义的接口进行通信的其它系统进行进一步的测试,从而构成最终完整的应用系统。
  • 验收测试,一般由客户主导,主要是将系统测试中的工作在用户现场重复执行,并在此基础上加入一些客户自己的测试愿望,比如采用完全真实的现场数据。

在这里插入图片描述
在这里插入图片描述

  • 白盒测试关注的是被测对象的内部构成细节,比如算法的结构和流程,所以多在类测试阶段采用。
  • 灰盒测试一般对应在集成测试阶段中使用,因为这个过程关注的是类、包等程序单元之间的关系,而不是类内部的细节。
  • 在系统或验收测试阶段一般使用的是黑盒的测试方法,这里系统内部的细节不再重要,重要的是系统的外部行为。

非功能测试

在这里插入图片描述

  • 除功能性测试,对软件系统的测试还包括非功能性测试,如性能测试、安全测试、安装测试、配置测试、界面测试、容量测试等。
  • 性能测试需要模拟实际用户负载来测试系统,包括反应速度、最大用户数、系统最优配置、软硬件性能、处理精度等。
  • 性能测试一般结合压力测试、负载测试等手段。

界面测试

  • 界面测试比较适合使用等价类的方法来建立对应的测试类。
  • 界面测试常采用“捕捉和回放”(Capture-and-Replay)工具。部分工具支持对输出屏幕的比较。
  • 测试用例的生成需要使用工具对被测界面通过一组标准的操作进行录制,然后在每个新的发布后对用例脚本进行回放,从而实现测试自动化的执行。

软件度量(*)

  • 度量是确定软件质量的一种有价值的辅助手段。
    • 一些质量度量相关的指标及作用:
    • 注释行数与代码行数的比例反映注释强度
    • 利用统计方法计算代码行数与方法数的比值确定出方法的平均长度
    • 保持变量和方法名适当的长度以提高程序的可读性
    • 方法中参数个数反映了方法的复杂程度
    • 类中实例变量的数目决定了该类信息的丰富程度
    • 继承深度提供了对继承使用是否恰当的信息,过多的继承对应深度的增加,并使得重用变得困难
  • 方法复杂程度的度量:McCabe指标(*)
  • 类的内聚性的度量:LCOM*指标(*)

统一描述/程序控制流图

在这里插入图片描述

  • 为尽量统一描述程序控制流图,如果指令对应的节点k1->k2->,…,->kn顺序出现,在以下情况将它们合并为一个节点进行处理:
    • 此序列的执行每次都是从K1开始,除此之外没有边终止于k2…kn;
    • 此序列的执行每次都是以Kn结尾,除此之外没有边始于k1…kn-1;
    • 满足1和2的最长节点序列。

McCabe环形复杂度

  • 以方法的控制流程图结构为基础进行计算:边数 - 节点数 + 2。
  • 考虑到复合条件的情况,McCabe的计算实际上反映了方法中下列语句产生的分支结构:if语句、条件组合&&和||、for语句和while 语句。
    在这里插入图片描述
  • 计算:
    -
    • 控制结构中分支或者循环,McCabe值越大,这也是程序可读性的一个反映指标。
    • 公式中的“+2”的主要作用是对McCabe值的归一化,以保证其最小值为1。
    • 在计算环形复杂度时需要注意,如果判断语句中含有多个原子谓词组合成的复合条件,那么需要将复合条件拆分成多个判定,并保证每个判定中只含有一个原子谓词。
    • 边数为9,节点数为7,环形复杂度V(G)=9-7+2=4。
  • 简化计算:
    在这里插入图片描述
    • 每个代码段初始复杂度为1
    • 遇到每个原子条件加1
    • 每个switch中的case段加1
  • 如果复杂度大于10,应考虑将该方法简化,而类中的方法McCabe值一般限制在5以下。
    • 优化算法结构,使其尽可能简单
    • 对if嵌套结构进行分解,将外层的if语句包含的程序部分移到另外一个局部方法中
    • 利用多态性使得对于分支的选择不再受开发者代码逻辑的控制,而是根据程序运行时的实际选择
  • 程序结构越复杂越难于测试和理解,复杂的逻辑条件同样会使方法的可理解性降低。

度量LCOM*(Lack of Cohesion in Methods)-

  • 度量LCOM*(Lack of Cohesion in Methods),分析每个类中方法与实例变量之间的关系,然后通过归一化公式进行计算。

  • 在这里插入图片描述

  • m为方法数,a为所含的实例变量数, u(Aj) 为访问每个实例变量的方法数。

  • 当LCOM为0时,该类的内聚性最佳,否则内聚性较差,需要考虑对其中的功能进行分解。当然如果该类只有一个唯一的实例变量,则不需要考虑它的LCOM
    在这里插入图片描述
    在这里插入图片描述

  • 由于get和set方法一般只对一个变量进行访问,为降低它们对LCOM*的影响,在计算时可不考虑类中的set和get方法。

测试方法(!)

等价类测试

  • 等价类是离散数学中的一个概念,其基本思想是将一个集合按照一定的标准划分为若干个子集合,其中每个元素的归属依赖于指定功能下具体的行为。
  • 对于数值型的集合,可根据输入变量的取值范围,产生一个有效等价类和两个无效等价类
  • 对于非数值类型的集合稍微复杂一些,比如对于枚举类型,假设合理的取值包括red、yellow和blue,则可以简单的将这些取值分别对应一个等价类,如果不允许其它值作为输入数据,则不存在它的无效等价类,例如Enumeration类型就是这样的情况。否则可以将所有其它输入对应一个无效等价类,例如包含任何符号的文本输入。

1.等价类方法

  • 考查一个类的构造方法,用来创建某学生对象的数据,具有名字、出生年份和专业3个属性。名字属性要求不能为空,出生年份要求介于1900和2000之间;专业取值只能是枚举类型,包括“贸易(TRADE)、计算机(CS)、数学(MATH)”中的一个元素。则对于输入数据可产生如下的等价类:
	E1) 名字非空(有效)
	E2) 名字为空(无效)
	E3) 出生年份小于1900(无效)
	E4) 出生年份大于等于1900并且小于等于2000(有效)
	E5) 出生年份大于2000(无效)
	E6) 专业为国际贸易(有效)
	E7) 专业为计算机科学(有效)
	E8) 专业为工科数学(有效)
  • 该方法有三个输入,进行调用时要同时指定这些参数,因此总的测试用例的数量与这些等价类的组合相关。
  • 一种组合方式是对于有效等价类要尽可能采用少的测试用例进行覆盖,比如对于E1、E4和E6三个有效等价类可使用一个测试用例同时覆盖。
  • 对于无效等价类则要慎重一些,其覆盖的规则是每个无效等价类必须与其它有效等价类组合测试,以此保证能够触发该无效值对应的专门处理过程。
    在这里插入图片描述

2.等价类与边界

  • 在等价类的基础上还可以继续应用边界值分析的方法
  • 数值类型的等价类上下边界容易确定,如E3的上边界1899和E5的下边界2001,E4的下边界1900和上边界2000。(D为下边界,U为上边界,加减号为边界附近,括号为已出现过)
  • 还需要注意计算机中对数值的表达方式,不同的数值类型其能够表达的最小值和最大值的能力也不尽相同。
    在这里插入图片描述

3.等价类与组合

  • 另外的组合方式是将3个变量具有的等价类分别进行组合,就会产生233=18个测试用例,这将涵盖所有可能的输入情况,把这种组合方式称为强等价类方法。
  • 这种测试用例的设计方法可以比较容易的实现,但前提条件是各个输入参数彼此独立并且随着等价类数目的增加所产生的
  • 试用例数量也会急剧增加,因此在实际测试中这种方法并不很常用。
  • 如果输入变量彼此间存在相互依赖的关系,则需要对业务规则进行仔细分析,才能达到最佳测试效果。

4.面向对象中的等价类

  • 类作为一个整体在使用等价类方法进行测试时要将状态作为一种输入参数进行考虑。
  • 类的状态是由其静态属性确定的,即实例变量。实例变量的数量决定了类的状态数量及其等价类的复杂程度。
  • 在一个示例性的预定系统中,每个客户的信用等级通过一个专门的类Credit进行管理,该信用等级与客户的支付行为相关。
    • 对超过一定金额的订单要求按照客户信用情况进行资金流动性的检查。
    • 在对该类进行测试时要考虑到3个状态对应的3个有效等价类和订单金额的组合情况。

基于控制流的测试

定义

  • 等价类测试方法关注被测对象向外界提供的功能,测试用例设计并不依赖程序内部结构,被称为是一种黑盒测试的方法。
  • 白盒测试的思想是充分利用程序的结构信息来设计测试用例,以实现对每个程序块(代码)的覆盖。覆盖程度和指标在等价类方法中通常无法保证和度量。
  • 基于控制流的测试将程序段使用一个有向控制流图进行描述。
    • 流图中的节点表示代码指令,节点间通过有向直线进行连接,表示这些指令执行的先后顺序。
    • If、Switch和循环指令会导致在图中对应节点处产生分支。

覆盖指标:

  • 程序覆盖是提供一组测试用例尽可能使得覆盖率指标越大越好,或者说越接近1越好。
  • 覆盖率指标有很多计算标准,其中较基础的有语句覆盖(Statement Coverage)、分支覆盖(Branch Coverage)、条件覆盖(Condition Coverage)、多条件组合覆盖(Multiple Condition Coverage)及路径覆盖(Path Coverage)等。
  • 语句覆盖:语句覆盖表示在程序控制流图中测试经过的节点数与所有节点数的比例
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 语句覆盖是一种很粗略的度量,因为它主要关注的是控制流图中的节点而非执行路径
  • 分支覆盖:分支覆盖的目标是尽可能覆盖控制流图中所有的边。
    • 在这里插入图片描述
    • 分支覆盖的意义在于它要求对所有程序片段间的各种可能的连接至少执行一次,因此,满足分支覆盖要求一定会满足语句覆盖要求。
    • 在这里插入图片描述
  • 条件覆盖:
    • 分支覆盖无法保证理论上所有可能的程序逻辑都会被测试到。
    • 条件覆盖:要求每个原子谓词的真假两种取值都要取到。
    • 条件覆盖不是根据程序的运行情况,而是根据出现的布尔条件进行测试用例的设计,条件覆盖与分支覆盖并没有直接的关系。
    • 在这里插入图片描述
    • 在这里插入图片描述
    • 高级编程语言中的短路(short-circuit)评估方式:
    if (x<4 || x/0==2)if (x<4 | x/0==2) [java]
    
    • 条件覆盖与分支覆盖没有任何特别的联系,这表示分支的完全覆盖不能保证条件的完全覆盖,反之也成立,即完全的条件覆盖也不能保证分支的完全覆盖。
    • 在分支覆盖和条件覆盖的基础上,还衍生出一种条件/分支覆盖标准,它是两种覆盖的混合,要求同时满足两种覆盖的要求。
  • 多条件组合覆盖:
    • 所有的覆盖要求在多条件组合覆盖标准中得到了综合,它要求所有在条件中出现的原子谓词的组合都要覆盖到。
    • 在这里插入图片描述
    • 在这里插入图片描述
  • 路径覆盖:
    • 绘制程序的控制流图。
    • 计算McCabe环形复杂度。从程序的环形复杂性可导出程序基本路径集合中的独立路径条数,独立路径要求在路径中至少含有一条未曾使用过的边。
    • 导出测试用例。为每一条基本路径设计测试用例的数据输入和预期结果,并确保覆盖到基本路径集中的每一条路径,参照环形复杂度规定的上限路径条数。
    • 在这里插入图片描述

测试实现技术

断言(Assertation)

  • 很多高级编程语言提供断言指令,如Java、C#和C++中,一般形式:Assert
  • 断言提供对异常进行检查的能力,但只能在开发阶段使用,并且不能替代常规的异常处理
  • 断言规定了方法必须要满足的条件,即需求的程序特性。如对输入不能确定是否满足既定要求,比如在防御性程序设计 (defensive programming)中要求方法总是可用,则必须要使用异常处理而不能使用断言
  • Java断言指令的扩展:
    assert <boolean condition>: <any object>
    
  • 位于函数尾部的断言可以用来检验是否该方法的计算是期望的结果,或者说可以用来检验计算出的结果是否具有期望的某些属性
  • assert false; 这个断言表示永远都不会通过,可将其放置于不应该到达的地方,以确保此处不可达。
  • 对断言的使用要确保不会带来任何的副作用,也就是说不会改变实际类的状态。
  • 比如,对于以下迭代器iter的断言是不合适的,因为该断言检测后会导致迭代器状态的改变,使其指向了下一个对象。
    assert iter.next()!=null;
    

测试框架之Junit

  • 首先由Kent Beck提出并完成的测试框架,允许使用各种程序设计语言来创建测试,而不是花费过多的精力在整个测试环境的创建上。
  • 首先在Smalltalk平台——SUnit框架,后来又在不同的语言平台上进行移植,如Java、C#、C++等,称为单元测试框架。
  • 基本思想是对不同的测试用例创建与其对应的测试方法,测试用例的执行和评价由JUnit接管。
  • 负责所有测试用例执行,得到执行的测试数、通过的测试数以及失败的测试数等。
  • JUnit可以使用反射机制在测试环境中调用传递过来的方法,所有某类的test测试方法都放在一个测试类中,这个类需要从JUnit提供的系统类TestCase继承而来。
  • 方法setup在每个测试开始之前执行,其中可以对实例变量等环境相关的内容进行设置。
  • 每个测试结束后会执行tearDown方法,比较适合做一些清理工作。
  • 对某些特征进行检验,通过调用源自类TestCase的父类中Assertion的方法

可测试性

  • 测试的构建一般采用的方式是“自底向上(Bottom Up)”的方式,也就是新的测试总是在已经经过测试的类和方法的基础上进行构建,比如要对一个依赖(使用)Discount类的另外一个类创建测试,则无需在这个测试类中重新测试Discount的price()方法。
  • 测试进行的过程中尤其是在多人并行开发的时候经常会出现彼此依赖的情况。为避免等待而影响开发进度,可以构建一个模拟程序(mock)或桩(stub)。
  • 通过上面的例子和分析,在进行类开发的时候就要为其可测试性做好设计上的准备:可测试性构建的原则。
    • 设计简单的方法
    • 避免私有方法
    • 优先使用通用方法
    • 组合优于继承
    • 避免隐藏的依赖关系与全局状态
    • 建设性质量保证
    • 人工测试

猜你喜欢

转载自blog.csdn.net/qq_42739587/article/details/114663049
今日推荐