第7章 面向对象的软件测试技术
7.1 面向对象测试概述
一、优点
对象概念对软件开发具有极大的好处,用户应用OOP(面向对象编程)技术可只编写一次代码而在今后反复重用,反之,则多半要在应用程序内部各个部分反复多次地编写同样的功能代码。故OOP减少了编写代码的总量,加快了开发的进度。
二、缺点
面向对象编程存在一些固有的缺点。例如:
(1)某个类被修改了,则所有依赖该类的代码都必须重新测试,而且,还可能需要重新修改依赖类以支持类的变更;
(2)如果相关开发文档没有得到仔细的维护,则很难确定哪些代码采用了父类。而且,如果在开发后期发现了软件中的错误,很可能会影响应用程序中绝大部分的代码。
三、结论
面向对象技术的基本思想表明该技术开发出来的软件应该有更高的质量,但由于无论采用何种编程技术,编程人员的错误都不可避免,且由于面向对象技术开发的软件代码重用率高,更需要严格的测试,避免错误的繁衍,因此软件测试需要新的测试理念和测试方法。
7.2 面向对象技术
一、基本概念
1.对象
现实世界中任何实体都可以看作是对象,面向对象是现实世界模型的自然延伸。对象之间通过消息相互作用。另外,现实世界中任何实体都可归属于某类事物,任何对象都是某一类事物的实例。面向对象的编程语言以对象为中心,以消息为驱动。用公式表示,过程式编程语言为:程序=算法+数据;面向对象编程语言为:程序=对象+消息。
2.类
类是面向对象设计的核心,类是实现抽象类型的工具,是通过抽象数据类型的方法来实现的一种新的数据类型。
3.类和对象的关系
类是对某一类对象的抽象,而对象是某一种类的实例,故类和对象是密切相关的。没有脱离对象的类,也没有不依赖于类的对象。
二、封装、继承和多态性
1.封装
(1)定义
封装是将数据和操作数据的函数衔接在一起,构成的一个具有类类型的对象的描述。封装把数据和操作结合成一体,使程序结构更加紧凑,同时避免数据紊乱带来的调试与维护的困难。
(2)要点
①封装要求一个对象应具备明确的功能,并且有一个或几个接口以便和其他对象相互作用。
②对象的内部实现(代码和数据)受保护,外界不能进行访问,只有对象中的代码才可访问该对象的内部数据。对象的内部数据结构的不可访问性称为数据隐藏。封装简化程序员对对象的使用,只需要知道输入什么和输出什么,对类内部进行何种操作不必追究。
例如,类Test(以C++为例):
输出结果是16,对于main()里的操作,无法更改类Test中的Value值,value是定义好的常量,这样保护了数据Value,实现了数据的隐藏。函数SetValue()和数据Value结合在一起操作,就实现了封装。
2.继承
(1)定义
继承即可以从一个类派生另一个类。派生类(也称为子类)继承了其父类和祖先类的数据成员和成员函数。派生类可增加新的属性和新的操作,另外,派生类在继承的成员函数不合适时也可以放弃不用,继承增加软件的可扩充性,并为代码重用提供强有力的手段。
(2)举例
水果是基类,具有好吃、营养等水果的基本特征;香蕉和苹果是水果的派生类,二者在继承水果类的一些特征的基础上,增加了如特殊的味道、形状等特征;红富士苹果和青苹果是苹果类的派生类,二者又增加各自的新特征。如此继承设计,可以大大减少工作量。
①程序举例
例如(C++程序),先设计水果类Friut:
②苹果类Apple需要与水果类Friut同样的成员函数void Eat()和void Taste(),另外增加新的函数void Color()和void Shape()。在设计时,不需要重写void Eat()和void Taste(),只需要继承水果类即可,苹果类的实际代码如下:
3.多态性
(1)定义
多态性即多种表现形式,具体表述为“一个对外接口,多个内在实现方法”。
多态性的实现,一般通过在派生类中重定义基类的虚函数来实现,使程序员在设计程序时可以对问题进行更好的抽象,易设计出重用性和维护性俱佳的程序。
(2)举例
如下例(C++程序),如果A是基类,B和C是A的派生类,基类中多态函数Test的参数是A的指针。则Test函数可以引用A、B、C的对象如下:
4.注意
(1)面向对象软件所独有的多态、继承、封装等新特点,使OO程序设计比传统语言程序设计产生错误的可能性更大,使得传统软件测试中的重点不再显得那么突出,也使原来测试经验和实践证明的次要方面成为主要问题。例如,在传统的面向过程程序中,对于函数y=Func(x);你只需要考虑一个函数“Func()”的行为特点,而在面向对象程序中,你不得不同时考虑基类函数“Base::Func()”的行为和继承类函数“Derived::Func()”的行为。
(2)面向对象程序的结构不再是传统的功能模块结构。作为一个整体,不能再用原有集成测试所要求的逐步将开发的模块搭建在一起进行测试的方法。而且,面向对象软件对各开发阶段都有不同于以往的要求和结果,已经不可能用功能细化的观点来检测面向对象分析和设计的结果了。故传统的测试模型对面向对象软件已经不再适用。于是产生了面向对象测试模型。
7.3 面向对象测试模型
面向对象的开发模型突破了传统的瀑布模型,面向对象的开发模型将开发分为面向对象分析(OOA),面向对象设计(OOD)和面向对象编程(OOP)三个阶段。针对该开发模型,结合传统的测试步骤的划分,把面向对象的软件测试分为:面向对象分析的测试、面向对象设计的测试、面向对象编程的测试、面向对象单元测试、面向对象集成测试、面向对象确认和系统测试。它是一种整个软件开发过程中不断测试的测试模型,使开发阶段的测试与编码完成后的单元测试、集成测试、系统测试成为一个整体。测试模型如图7-1所示。
图7-1 面向对象测试模型
OOA Test和OOD Test是对分析结果和设计结果的测试,主要是对分析设计产生的文本进行的,是软件开发前期的关键性测试。
OOP Test主要针对编程风格和程序代码实现进行测试,其主要的测试内容体现在以下两方面:
一、面向对象单元测试
面向对象单元测试对程序内部具体单一的功能模块的测试,主要是对类成员函数的测试,是进行面向对象集成测试的基础。
二、面向对象集成测试
面向对象集成测试主要对系统内部的相互服务进行测试,如成员函数间的相互作用,类间的消息传递等。不但要基于面向对象单元测试,更要参考OOD或OOD Test结果。确认和系统测试是基于面向对象集成测试的最后阶段的测试,主要以用户需求为测试标准,需要借鉴OOA或OOA Test的结果。
尽管上述各阶段测试构成一个相互作用的整体,但其测试的主体、方向和方法各有不同。
7.4 面向对象软件的测试策略
一、面向对象分析(OOA)的测试
1.概述
(1)定义
传统的面向过程分析是一个功能分解的过程,是一个把系统看成可以分解的功能的集合,面向对象分析(OOA)则大不同与传统的面向过程分析,它是把E—R图和语义网络模型,即信息造型中的概念,与面向对象程序设计语言中的重要概念结合在一起而形成的分析方法。
(2)测试重点
OOA直接映射问题空间,全面地将问题空间中实现功能的现实抽象化。将问题空间中的实例抽象为对象(不同于C++中对象的概念),用对象的结构反映问题空间的复杂实例和复杂关系,用属性和服务表示实例的特性和行为。对OOA测试重点在于它的完整性和冗余性,原因如下:
①完整性:OOA的结果是为后面阶段类的选定和实现,类层次结构的组织和实现做准备。OOA对问题空间的分析抽象不完整,最终会影响软件的功能实现,会导致软件开发后期产生大量原本可避免的修补工作。
②冗余性:一些冗余的对象或结构会影响类的选定、程序的整体结构或增加程序员不必要的工作量。
(3)OOA阶段的测试的划分
OOA的测试是一个不可分割的系统过程,为叙述的方便,鉴于Coad方法所提出的OOA实现步骤,对OOA阶段的测试划分为以下五个方面:
①对认定的对象的测试;
②对认定的结构的测试;
③对认定的主题的测试;
④对定义的属性和实例关联的测试;
⑤对定义的服务和消息关联的测试。
2.对认定的对象的测试
OOA中认定的对象是对问题空间中的结构,其他系统、设备,被记忆的事件,系统涉及的人员等实际实例的抽象。对其测试可以从以下几个方面考虑:
(1)认定的对象是否全面,问题空间中所有涉及到的实例是否都反映在认定的抽象对象中;
(2)认定的对象是否具有多个属性,只有一个属性的对象通常应看成其他对象的属性,而不是抽象为独立的对象;
(3)对认定为同一对象的实例是否有共同的、区别于其他实例的共同属性;
(4)对认定为同一对象的实例是否提供或需要相同的服务。如果服务随着实例的不同而变化,认定的对象就需要分解或利用继承性来分类表示;
(5)如果系统没有必要始终保持对象代表的实例的信息,提供或者得到关于其服务、认定的对象也无必要;
(6)认定的对象的名称应该尽量准确、适用。
3.对认定的结构的测试
在Coad方法中,认定的结构指的是多种对象的组织方式,用来反映问题空间中的复杂实例和复杂关系。该结构分为以下两种:
(1)分类结构
分类结构体现了问题空间中的实例一般与特殊的关系,对认定的分类结构的测试可从以下几个方面着手:
①对于结构中的一种对象,尤其是处于高层的对象,是否在问题空间中含有不同于下一层对象的特殊的可能性,即是否能派生出下一层对象;
②对于结构中的一种对象,尤其是处于同一低层的对象,是否能抽象出在现实中有意义的更一般的上层对象;
③对所有认定的对象,是否能在问题空间内向上层抽象出在现实中有意义的对象;
④高层的对象的特性是否完全体现下层的共性;
⑤低层的对象是否在高层特性基础上有其特殊性。
(2)组装结构
组装结构体现了问题空间中实例整体与局部的关系。对认定的组装结构的测试从以下几个方面入手:
①整体(对象)和部件(对象)的组装关系是否符合现实的关系;
②整体(对象)的部件(对象)是否在考虑的问题空间中有实际应用;
③整体(对象)中是否遗漏了在问题空间中有用的部件(对象);
④部件(对象)是否能够在问题空间中组装新的有现实意义的整体(对象)。
4.对认定的主题的测试
主题是在对象和结构的基础上更高一层的抽象。为了提供OOA分析结果的可见性,对主题层的测试应该考虑以下几个方面:
(1)贯彻George Miller的“7+2”原则(George Miller指出普通人的短期记忆的能量,是7加2或减2,即5与9之间)。如果主题个数超过7个,就要求对有较密切属性和服务的主题进行归并;
(2)主题所反映的一组对象和结构是否具有相同和相近的属性和服务;
(3)认定的主题是否是对象和结构更高层的抽象,是否便于理解OOA结果的概貌(尤其是对非技术人员的OOA结果读者);
(4)主题间的消息联系(抽象)是否代表主题所反映的对象和结构之间的所有关联。
5.对定义的属性和实例关联的测试
属性是用来描述对象或结构所反映的实例的特性的,而实例关联是反映实例集合间的映射关系。对属性和实例关联的测试从以下几个方面考虑:
(1)定义的属性是否适用于相应的对象和分类结构的各现实实例;
(2)定义的属性在现实世界是否与该实例关系密切;
(3)定义的属性在问题空间是否与该实例关系密切;
(4)定义的属性是否能不依赖于其他属性被独立理解;
(5)定义的属性在分类结构中的位置是否恰当以及低层对象的共有属性是否在上层对象的属性中体现;
(6)在问题空间中各对象的属性是否定义完整;
(7)定义的实例关联是否符合现实;
(8)在问题空间中实例关联是否定义完整,特别需要注意“一对多”和“多对多”的实例关联。
6.对定义的服务和消息关联的测试
定义的服务,即定义的每一种对象和结构在问题空间所要求的行为。由于问题空间中实例间必要的通信,在OOA中需要相应地定义消息关联。对定义的服务和消息关联的测试从如下方面进行:
(1)对象和结构在问题空间的不同状态下是否定义了相应的服务;
(2)对象或结构所需要的服务是否都定义了相应的消息关联;
(3)定义的消息关联所指引的服务提供是否正确;
(4)沿着消息关联执行的线程是否合理,是否符合现实过程;
(5)定义的服务是否重复,是否定义了能够得到的服务。
二、面向对象设计(OOD)的测试
1.概述
在以往结构化的设计方法,用的“是面向作业的设计方法,它把系统分解以后,提出一组作业,这些作业是以过程实现系统的基础构造,把问题域的分析转化为求解域的设计,分析的结果是设计阶段的输入”。
面向对象设计(OOD)采用“造型的观点”,以OOA为基础归纳类,并建立类结构或进一步构造成类库,以实现分析结果对问题空间的抽象。
2.特点
(1)概述
OOD归纳的类可以是对象简单的延续,也可以是不同对象的相同或相似的服务。OOD是OOA的进一步细化和更高层的抽象,它们的界限一般很难严格区分。
OOD确定类和类结构不仅满足当前需求分析的要求,而且通过重新组合或加以适当的补充,能方便实现功能的重用和扩增,以不断适应用户的要求。故对OOD的测试,建议针对功能的实现和重用以及对OOA结果的拓展,从以下三方面考虑:
①对认定的类的测试;
②对构造的类层次结构的测试。
③对类库的支持的测试。
(2)对认定的类的测试
OOD认定的类可以是OOA中认定的对象,也可以是对象所需要的服务的抽象,对象所具有的属性的抽象。认定的类原则上应该尽量具有基础性,才便于维护和重用。主要包括以下几个方面:
①是否涵盖OOA中所有认定的对象;
②是否能体现OOA中定义的属性;
③是否能实现OOA中定义的服务;
④是否对应着一个含义明确的数据抽象;
⑤是否尽可能少地依赖其他类;
⑥类中的方法(在C++中即类的成员函数)是否是单用途。
(3)对构造的类层次结构的测试
为能充分发挥面向对象的继承共享特性,OOD的类层次结构,通常基于OOA中产生的分类结构的原则来组织,着重体现父类和子类间的一般性和特殊性。当前问题空间,对类层次结构的主要要求是能了解空间构造实现全部功能的结构框架。故需要测试以下几个方面:
①类层次结构是否涵盖了所有定义的类;
②是否能体现OOA中所定义的实例关联;
③是否能实现OOA中所定义的消息关联;
④子类是否具有父类没有的新特性;
⑤子类间的共同特性是否完全在父类中得以体现。
(4)对类库支持的测试
对类库的支持虽然也属于类层次结构的组织问题,但其强调的重点是软件再次开发的重用。由于并不直接影响当前软件的开发和功能实现,将其单独提出来测试,也可作为对高质量类层次结构的评估。
①拟定测试点
a.一组子类中关于某种含义相同或基本相同的操作,是否有相同的接口(包括名字和参数表);
b.类中方法(在C++中即类的成员函数)的功能是否较单纯,相应的代码行是否较少(建议为不超过30行);
c.类的层次结构是否深度大、宽度小。
因为OOA、OOD阶段所建立的分析和设计模型不能进行传统意义上的测试,故不能被执行,所以在每次迭代之后,一定要进行评审。评审主要针对正确性和一致性方面。
在正确性方面,OO开发模式为演化(重复迭代)性质,即系统的初期为非形式化表示,以后发展为类的细节模型、类的连接和关联,系统设计和配置,以及对类的设计。每阶段都要进行评审。
正确性主要在于分析和设计模型表示所使用的符号语法是否正确,语义是否正确,以及类的关联(实例间的联系)是否正确地反映了真实世界对象间的关联。
在一致性方面,由于演化性质,OOA和OOD模型(包括分析、设计和编码层次,即类、属性、操作、消息)不仅要正确,而且要一致。一致性可以用模型内各实体间的关联性来判断。为评估一致性,应检查每个类及其与其他类的连接。可运用类——责任——协作者(CRC)模型和对象——关系图。
CRC模型由CRC索引卡片构成,各CRC卡片列出类名、类的责任(操作)、以及其协作者类,类向其发送消息并依赖其完成自己的责任。协作包含在OO系统的类之间的一系列关系(即连接),对象关系模型提供了类之间连接的图形表示。所有这些信息可以从OOA模型得到。
②评估类模型推荐采用的步骤
a.再次考察CRC模型和对象——关系模型,进行交叉检查,以保证由OOA模型所蕴含的协作适当地反应在二者中;
b.检查各CRC索引卡片的描述,以确定某被授权的责任是否是协作者定义的一部分,例如,某个POS结账系统定义的类,称为credit sale,该类的CRC卡片如图7-2所示。
图7-2 一个用于复审的CRC卡片例子
对于这组类和协作,如果某责任被委托给指定的协作者(credit card),该责任是否将被完成,即类credit card是否具有一个操作,使得其可以被读。本例的情形是是。遍历对象关系,以保证所有这样的连接有效;
c.反转该连接,以保证各被请求服务的协作者正在接收来自合理源的请求;
d.使用在c步检查的反转连接,确定是否可能需要其他的类,或责任是否被合适地在类间分组;
e.确定被广泛请求的责任是否可被组合为单个的责任,例如,read credit card和get authorization在每种情况中均发生,可以被组合为validate credit。request责任,结合了获取信用卡号及获得授权;
f.步骤a~e被迭代地应用到各类,并贯穿于OOA模型的每次演化过程中。一旦已经创建了设计模型,也应该进行对系统设计和对象设计的复审。
系统设计描述了构成产品的子系统、子系统被分配到处理器的方式,以及类到子系统的分配。对象模型表示各类的细节和实现类间的协作所必须的消息序列活动。
通过检查在OOA阶段开发的对象——行为模型,映射需要的系统行为和被设计用于完成该行为的子系统来进行系统设计的复审,也在系统行为的语境内复审并发性和任务分配,评估系统的行为状态以确定哪些行为并发地存在。
对象模型应该针对对象——关系网络来测试,以保证所有设计对象包含为实现每张CRC索引卡片定义的协作所必须的属性和操作。
三、面向对象编程(OOP)的测试
1.简介
面向对象程序所具有的继承、封装和多态的新特性,使得传统的测试策略不再完全适。
封装是对数据的隐藏,外界只能通过被提供的操作来访问或修改数据,这样降低了数据被任意修改和读写的可能性,降低了传统程序中对数据非法操作的测试;继承是面向对象程序的重要特点,继承使得代码的重用率提高,同时也使错误传播的概率提高;多态使得面向对象程序对外呈现出强大的处理能力,但同时却使得程序内“同一”函数的行为复杂化,测试时不得不考虑不同类型具体执行的代码和产生的行为;
在面向对象编程(OOP)阶段,忽略类功能实现的细则,将测试集中在类功能的实现和相应的面向对象程序风格上,主要体现为以下两个方面(假设编程使用C++语言):
(1)数据成员是否满足数据封装的要求;
(2)类是否实现了要求的功能。
2.数据成员是否满足数据封装的要求
数据封装是数据和数据有关的操作的集合。检查数据成员是否满足数据封装的要求,基本原则是数据成员是否被外界(数据成员所属的类或子类以外的调用)直接调用。即当改变数据成员的结构时,是否影响类的对外接口,是否会导致相应的外界必须改动。
注意,有时强制的类型转换会破坏数据的封装特性。例如:
在上面的程序段中,xx的私有数据成员a和b可以通过yy被随意访问。
3.类是否实现要求的功能
类所实现的功能都是通过类的成员函数执行的,应首先保证类成员函数的正确性,单独地看待类的成员函数,几乎所有传统的单元测试中所使用的方法,都可在面向对象的单元测试中使用。类函数成员的正确行为是类能够实现要求的功能的基础,类成员函数间的作用和类之间的服务调用是单元测试无法确定的。因此,需要进行面向对象的集成测试。测试类的功能,应该以所做的OOD结果为依据,检测类提供的功能是否满足设计的要求。必要时还应该参照OOA的结果,以之为最终标准。
四、面向对象软件的单元测试
传统的单元测试是针对程序的函数、过程或完成某一特定功能的程序块,可沿用单元测试的概念,来实际测试类成员函数。一些传统的测试方法在面向对象的单元测试中都可以使用。单元测试一般建议由程序员完成。用于单元级测试的测试分析(提出相应的测试要求)和测试用例(选择适当的输入,达到测试要求),规模和难度等均远小于后面介绍的对整个系统的测试分析和测试用例,而且强调对语句应该有100%的代码执行覆盖率。
1.两个假设
在设计测试用例选择输入数据时,可基于以下两个假设(假设使用C++编程语言):
(1)如果函数(程序)对某一类输入中的一个数据能正确执行,对同类中的其他输入也能正确执行;
(2)如果函数(程序)对某一复杂度的输入能正确执行,对更高复杂度的输入也能正确执行。例如,需要选择字符串作为输入时,基于本假设,就无须计较字符串的长度。除非字符串的长度是要求固定的,如IP地址字符串。
在面向对象程序中,类成员函数通常都很小,功能单一,函数之间调用频繁,容易出现一些不宜发现的错误。例如:
if(-1==write(fid,buffer,amount))error_out();
该语句没有全面检查write()的返回值,无意中只断然假设了数据被完全写入和没有写入两种情况。当测试也忽略了数据部分的写入情况时,就给程序遗留了隐患,建议加入else语句。
①按程序的设计,使用函数strrchr()查找最后的匹配字符,但错误程序中写成了函数strchr(),使程序功能实现时查找的是第一个匹配字符;
②程序中将if(strncmp(str1,str2,strlen(str1)))误写成了if(strncmp(str1,str2,strlen(str2)))。如果测试用例中使用的数据str1和str2长度一样,就无法检测出来。因此,在做测试分析和测试用例设计时,应该注意面向对象程序的特点,仔细地进行测试分析和设计测试用例,尤其是针对以函数返回值作为条件判断的选择、字符串操作等情况。
2.两方面考虑
面向对象编程的特性使得对成员函数的测试,不完全等同于传统的函数或过程测试。尤其是继承特性和多态特性,使子类继承或过载的父类成员函数出现了传统测试中未遇见的问题。Brian Marick给出了两方面的考虑如下:
(1)继承的成员函数是否都不需要测试
对父类中已经测试过的成员函数,有以下两种情况需要在子类中重新测试:
①继承的成员函数在子类中做了改动;
②成员函数调用了改动过的成员函数的一部分。
例如:假设父类Bass有两个成员函数:Inherited()和Redefined(),子类Derived只对Redefined()做了改动。Derived::Redefined()显然需要重新测试。对于Derived::Inherited(),如果它有调用Redefined()的语句(如:x=xfRedefined()),就需要重新测试,反之,无此必要。
(2)对父类的测试是否能照搬到子类
延用上面的假设,Base::Redefined()和Derived::Redefined()已经是不同的成员函数,其有不同的服务说明和执行。对此,照理应该对Derived::Redefined()重新进行测试分析,设计测试用例。但由于面向对象的继承使得两个函数有相似之处,故只需在Base::Redefined()的测试要求和测试用例上添加对Derived::Redfined()新的测试要求和增补相应的测试用例。例如:Base::Redefined()函数中含有如下语句:
在原有的测试上,对Derived::Redfined()的测试只需做如下改动:将value==0的测试结果期望改动;增加value==99的测试。
3.多态的不同形式
多态有几种不同的形式,如参数多态、包含多态、过载多态。包含多态和过载多态在面向对象语言中通常体现在子类与父类的继承关系上。包含多态虽然使成员函数的参数可有多种类型,但通常只是增加测试的复杂度。对具有包含多态的成员函数进行测试时,只需要在原有的测试分析和基础上增加对测试用例中输入数据的类型的考虑即可。
五、面向对象软件的集成测试
因为面向对象软件没有层次的控制结构,传统的自顶向下和自底向上集成策略就没有意义,而且,一次集成一个操作到类中(传统的增量集成方法)经常是不可能的。此外,传统的自底向上通过集成完成的功能模块测试,一般可以在部分程序编译完成的情况下进行。
1.简介
面向对象的集成测试通常需要在整个程序编译完成后进行。此外,面向对象程序具有动态特性,程序的控制流往往无法确定,故只能对整个编译后的程序做基于黑盒子的集成测试。
2.对OO软件的集成测试的策略
(1)基于线程的测试(threadbased testing)
集成对回应系统的一个输入或事件所需的一组类,各线程被集成并分别测试,应用回归测试以保证没有产生副作用。
(2)基于使用的测试(use based testing)
通过测试那些几乎不使用服务器类的类(称为独立类)而开始构造系统,在独立类测试完成后,下一层中使用独立类的类(称为依赖类)被测试。该依赖类层次的测试序列一直持续到构造出完整一个系统。序列和传统集成不同,要尽可能避免使用驱动器和桩(stubs)作为替代操作。
3.面向对象的集成测试的步骤
面向对象的集成测试能够检测出相对独立的,单元测试无法检测出,那些类相互作用时才会产生的错误。面向对象的集成测试可以分成以下两步进行:
(1)静态测试
静态测试主要针对程序的结构进行,检测程序结构是否符合设计要求。现在流行的一些测试软件都能提供一种称为“可逆性工程”的功能,即通过源程序得到类关系图和函数功能调用关系图,将“可逆性工程”得到的结果与OOD的结果相比较,检测程序结构和实现上是否有缺陷。即通过这种方法检测OOP是否达到设计要求。
(2)动态测试
动态测试设计测试用例时,通常需要上述的功能调用结构图、类关系图或者实体关系图为参考,确定不需要被重复测试的部分,从而优化测试用例,减少测试工作量,使得进行的测试能够达到一定覆盖标准。
①测试要达到的覆盖标准
a.达到类所有的服务要求或服务提供的一定覆盖率;
b.依据类间传递的消息,达到对所有执行线程的一定覆盖率;
c.达到类的所有状态的一定覆盖率;
d.可以考虑使用现有的一些测试工具来得到程序代码执行的覆盖率。
②具体设计测试用例可参考的步骤
a.先选定检测的类,参考OOD分析结果,仔细确定出类的状态和相应的行为,类或成员函数间传递的消息,输入或输出的界定等;
b.确定覆盖标准;
c.利用结构关系图确定待测类的所有关联;
d.根据程序中类的对象构造测试用例,确认使用何种输入激发类的状态,使用类的服务和期望产生何种行为等。
注意:设计测试用例时,不但要设计满足类功能的输入,还应有意识地设计一些被禁止的例子,确认类是否有不合法的行为产生,如发送与类状态不相适应的消息,要求不相适应的服务等。根据具体情况,动态的集成测试,有时也可通过系统测试完成。
六、面向对象软件的确认和系统测试
通过单元测试和集成测试,仅能保证软件开发的功能得以实现,但不能确认在实际运行时,它是否能够满足用户的需要,是否大量地存在着实际使用条件下会被诱发产生错误的隐患。为此,对完成开发的软件必须进行规范的系统测试,即需要测试其与系统其他部分配套运行的表现,以确保在系统各部分协调工作的环境下软件也能正常运行。测试过程中,需要注意以下几个方面:
(1)系统测试应该尽量搭建与用户实际使用环境相同的测试平台,应该保证被测系统的完整性,对暂时没有的系统设备部件,应采取相应的模拟手段。
(2)系统测试时,应该参考OOA分析的结果,对应描述的对象、属性和各种服务,检测软件是否能够完全“再现”问题空间。系统测试不仅是检测软件的整体行为表现,也是对软件开发设计的再确认。
(3)在系统层次,类连接的细节消失了。OO软件的有效性集中在用户可见的动作和用户可识别的系统输出上。为协助有效性测试的导出,测试人员应该利用作为分析模型一部分的使用实例,使用实例提供在用户交互需求中很可能发现错误的一场景。
OO软件确认和系统测试具体的测试内容与传统系统测试基本相同,包括:功能测试、强度测试、性能测试、安全测试、恢复测试、易用性测试、安装/卸载测试(install/uninstall test)等。
7.5 面向对象软件测试用例设计
一、简介
面向对象测试关注于设计合适的操作序列以测试类的状态。Berard提出了测试用例的设计方法,主要原则包括:
(1)对每个测试用例应当给予特殊的标识,并且还应当与测试的类有明确的联系;
(2)测试目的应当明确;
(3)应当为每个测试用例开发一个测试步骤列表。
该列表应包含以下内容:
(1)列出所要测试的对象的专门说明;
(2)列出将要作为测试结果运行的消息和操作;
(3)列出测试对象可能发生的例外情况;
(4)列出外部条件(即为了正确对软件进行测试所必须有的外部环境的变化);
(5)列出为了帮助理解和实现测试所需要的附加信息。
二、传统测试用例设计方法的可用性
尽管OO软件的局域性、封装性、信息隐藏、继承性和对象的抽象性等给测试用例设计带来困难,但黑盒测试技术不仅适用于传统软件,也适用于OO软件测试,use cases可以为黑盒及基于状态的测试的设计提供有用的输入。白盒测试用于OO软件类的操作定义,基本路径、循环测试或数据流技术,可帮助保证已经测试了操作中的每条语句。但OO软件中许多类的操作结构简单明了,故被认为在类层上测试可能要比传统软件中的白盒测试方便。
三、基于故障的测试
1.简介
在OO软件中,基于故障的测试具有较高的发现可能故障的能力。由于系统以满足用户的需求为目的,因此,基于故障的测试要从分析模型开始,考察可能发生的故障。为确定这些故障是否存在,可设计用例去执行设计或代码。
2.举例
软件开发人员经常忽略问题的边界,例如,当测试Divide操作(该操作对负数和零返回错误)时,考虑边界,“零本身”用于检查程序员是否犯了如下错误:
if(x>0)divide();
而不是正确的:
if(x>=0)divide();
3.特点
(1)该方法的有效性依赖于测试人员如何感觉“似乎可能的故障”,如果OO系统中的真实故障被感觉为“难以置信的”,则本方法实质上不比任何随机测试技术好,但如果可以从分析和设计模型进行深入的检查,则基于故障的测试则可以用相当低的工作量来发现大量的错误。
(2)基于故障测试可以用于集成测试,集成测试可以发现消息联系中“可能的故障”。“可能的故障”一般为意料之外的结果、错误地使用操作、不正确的引用等。为确定由操作(功能)引起的可能故障必须检查操作的行为。
(3)该方法可用于操作测试,还可用于属性测试,用以确定其对于不同类型的对象行为是否赋予了正确的属性值,因为一个对象的“属性”由其赋予属性的值定义。
注意:集成测试是从客户对象(主动),而不是从服务器对象(被动)上发现错误。正如传统的软件集成测试是把注意力集中在调用代码,而不是被调用代码一样,即发现客户对象中“可能的故障”。
四、基于场景的测试
1.概述
(1)简介
基于场景的测试主要关注用户需要做什么,而不是产品能做什么,即从用户任务中找出用户要做什么及如何去执行。
该基于场景的测试有助于在一个单元测试情况下检查多重系统。所以基于场景测试用例的测试比基于故障测试的更实际(接近用户),而且更复杂一点。
(2)举例
例子描述:考察一个OA软件的基于场景测试的用例设计。
使用用例:确定最终设计。
背景:打印最终设计,并能从预览窗口中发现一些不易察觉的错误。
其执行事件序列:打印整个文件;对文件进行剪切、复制、粘贴、移动等修改操作;修改文件后,进行文件预览;进行多页预览。
显然,测试人员希望发现预览和编辑这两个软件功能是否能够相互依赖,否则就会产生错误。
2.基于故障的测试减少的错误类型
基于故障的测试减少了两种主要类型的错误:
(1)不正确的规格说明,如做了用户不需要的功能,也可能缺少用户需要的功能;
(2)没有考虑子系统间的交互作用,如一个子系统(事件或数据流等)的建立,导致其他子系统的失败。
五、OO类的随机测试
1.定义
如果一个类有多个操作(功能),这些操作(功能)序列有多种排列。而不变化的操作序列可随机产生,用这种可随机排列的序列来检查不同类实例的生存史,称为随机测试。
2.举例
一个银行信用卡的应用,其中有一个类:账户(account)。该account的操作有open、setup、deposit、withdraw、balance、summarize、creditlimit和close。这些操作中的每项都可用于计算,但open、close必须在其他计算的任何一个操作前后执行,即使open和close有该限制,这些操作仍有多种排列,所以一个不同变化的操作序列可由于应用不同而随机产生,如一个account实例的最小行为转换期可包括以下操作:
open+setup+deposit+withdraw+close
对account的最小测试序列。在下面序列中可能发生大量的其他行为:
open+setup+deposit+[deposit|wi
由此可以随机产生一系列不同的操作序列,例如:
测试用例1:
open+setup+deposit+deposit+balance+summarize+withdraw+close
测试用例2:open+setup+deposit+withdraw+deposit+balance+creditLimit+withdraw+close
可以执行这些测试和其他的随机顺序测试,以测试不同的类实例生命历史。
六、类层次的分割测试
该测试可减少用完全相同的方式检查类测试用例的数目,类似传统测试中的等价类划分测试。分割测试可分为以下三种:
1.基于状态的分割
按类操作是否改变类的状态来进行分割(归类),仍用account类为例,改变状态的操作有deposit、withdraw,不改变状态的操作有balance、summarize、creditlimit。如果测试按检查类操作是否改变类状态来设计,则结果如下:
[用例1]:执行操作改变状态
open+setup+deposit+deposit+withdraw+withdraw+close
[用例2]:执行操作不改变状态
open+setup+deposit+summarize+creditlimit+withdraw+close
2.基于属性的分割
按类操作所用到的属性来分割(归类),仍以account类为例,其属性creditlimit能被分割为三种操作:用creditlimit的操作,修改creditlimit的操作,不用也不修改creditlimit的操作。这样,测试序列就可按每种分割来设计。
3.基于类型的分割
按完成的功能分割(归类)。例如,在account类的操作中,可以分割为初始操作:open、setup;计算操作:deposit、withdraw;查询操作:balance、summarize、creditlimit;终止操作:close。
七、由行为模型(状态、活动、顺序和合作图)导出的测试
1.简介
状态转换图(STD)是可用来帮助导出类的动态行为的测试序列,以及这些类与之合作的类的动态行为的测试序列。
2.举例
以account类为例,开始由empty acct状态转换为setup acct状态。类实例的大多数行为发生在working acct状态中。最后,取款和关闭分别使account类转换到non-working acct和dead acct状态。状态转换图(STD)如图7-3所示。
图7-3 状态转换图(STD)
这样,设计的测试用例就能完成所有的状态转换,即操作序列应当能使account类所有允许的状态进行转换。
(1)测试用例1
open+setupAcct+deposit(initial)+withdraw(final)+close;
应该注意,该序列等同于一个最小测试序列,可加入其他测试序列到最小序列中。
(2)测试用例2
open+setupAccnt+deposit(initial)+deposit+balance+credit+withdraw(final)+close;
(3)测试用例3
open+setupAccnt+deposit(initial)+deposit+withdraw+accntlnfo+withdraw(final)+close。
还可导出更多的测试用例,以保证该类所有行为被充分检查。在类行为导致与一个或多个类协作的情况下,可使用多个STD去跟踪系统的行为流。
3.面向对象测试的整体目标
即以最小的工作量发现最多的错误,和传统软件测试的目标是一致的,但是由于OO软件具有的特殊性质,在测试的策略和战术上有很大不同。测试的视角扩大到包括复审分析和设计模型,此外,测试的焦点从过程构件(模块)移向类。
(1)OOA(Object-Oriented Analysis)和OOD(Object-Oriented Design)的评审与传统软件的分析和设计相同,应给出相应的评审检查表;
(2)OOP(Object-Oriented Programming)后,单元和组装测试策略必须作相应的改变;
(3)测试用例设计必须说明OO软件特有的性质。