实现
通常把编码和测试统称为实现。
程序的质量主要取决于软件设计的质量,但是,所选用的程序设计语言的特点及编码风格也将对程序的可靠性、可读性、可测试性和可维护性产生深远的影响。
测试的目的就是在软件投入生产性运行之前,尽可能多地发现软件中的错误。目前软件测试仍然是保证软件质量的关键步骤,它是对软件规格说明、设计和编码的最后复审。
7.1 编码
7.1.1 选择程序设计语言
程序设计语言的特点会影响人的思维和解题方式,会影响人和计算机通信的方式和质量,也会影响其他人阅读和理解程序的难易程度。
编码之前的选择一种适当的程序设计语言是一项重要工作。
高级语言明显优于汇编语言,在需要效率的某些场合,需要汇编语言,或者大型系统中执行时间非常关键的(或直接依赖于硬件的)一小部分代码需要用汇编语言书写之外,其他程序应该一律用高级语言书写。
有理想的模块化机制,以及可读性好的控制结构和数据结构;为了便于调试和提高软件可靠性,语言特点应该使编译程序能够尽可能多地发现程序中的错误;为了降低软件开发和维护的成本,选用的高级语言应该有良好的独立编译机制。
选择语言的主要实现标准:
(1) 系统用户的要求。如果所开发的系统由用户负责维护,用户通常要求用他们熟悉的语言书写程序。
(2) 可以使用的编译程序。运行目标系统的环境中可以提供的编译程序往往限制了可以选用的语言的范围。
(3) 可以得到的软件工具。如果某种语言有支持程序开发的软件工具可以利用,则目标系统的实现和验证都变得比较容易。
(4) 工程规模。如果工程规模很庞大,现有的语言又不完全适用,那么设计并实现一种供这个工程项目专用的程序设计语言,可能是一个正确的选择。
(5) 程序员的知识。虽然对于有经验的程序员来说,学习一种新语言并不困难,但是要完全掌握一种新语言却需要实践。如果和其他标准不矛盾,那么应该选择一种已经为程序员所熟悉的语言。
(6) 软件可移植性要求。如果目标系统将在几台不同的计算机上运行,或者预期的使用寿命很长,那么选择一种标准化程度高、程序可移植性好的语言就是很重要的。
(7) 软件的应用领域。所谓的通用程序设计语言实际上并不是对所有应用领域都同样适用。因此,选择语言时应该充分考虑目标系统的应用范围。
7.1.2 编码风格
源程序代码的逻辑简明清晰、易读易懂应该遵循下述规则:
1.程序内部的文档
包括恰当的标识符、适当的注解和程序的视觉组织等等。
选取含义鲜明的名字,使它能正确地提示程序对象所代表的实体,这对于帮助阅读者理解程序是很重要的。如果使用缩写,那么缩写规则应该一致,并且应该给每个名字加注解。
注解非常有助于对程序的理解。
每个模块开始处有序言性的注解:简要描述模块的功能、主要算法、接口特点、重要数据以及开发简史;
程序中间与一段程序代码有关的注解:主要解释包含这段代码的必要性。
不能滥用注释,应利用注解提供一些额外的信息。注解的内容一定要正确。
程序清单的布局对于程序的可读性也有很大影响,应该利用适当的阶梯形式使程序的层次结构清晰明显。
2.数据说明
数据说明的次序应该标准化。有次序就容易查阅,因此能够加速测试、调试和维护的过程。
当多个变量名在一个语句中说明时,应该按字母顺序排列这些变量。
如果设计时使用了一个复杂的数据结构,则应该用注解说明用程序设计语言实现这个数据结构的方法和特点。
3. 语句构造
构造语句时应该遵循的原则是,每个语句都应该简单而直接,不能为了提高效率而使程序变得过分复杂。下述规则有助于使语句简单明了:
- 不要为了节省空间而把多个语句写在同一行; - 尽量避免复杂的条件测试;
- 尽量减少对“非”条件的测试;
- 避免大量使用循环嵌套和条件嵌套;
- 利用括号使逻辑表达式或算术表达式的运算次序清晰直观。
4. 输入输出
在设计和编写程序时应该考虑下述有关输入输出风格的规则:
- 对所有输入数据都进行检验;
- 检查输入项重要组合的合法性;
- 保持输入格式简单;
- 使用数据结束标记,不要要求用户指定数据的数目;
- 明确提示交互式输入的请求,详细说明可用的选择或边界数值;
- 当程序设计语言对格式有严格要求时,应保持输入格式一致;
- 设计良好的输出报表;
- 给所有输出数据加标志。
5. 效率
效率主要指处理机时间和存储器容量两个方面。
应该清晰3条概念:
- 首先,效率是性能要求,因此应该在需求分析阶段确定效率方面的要求。软件应该像对它要求的那样有效,而不应该如同人类可能做到的那样有效(需求分析相关);
- 其次,效率是靠好设计来提高的(设计相关);
- 第三,程序的效率和程序的简单程度是一致的,不要牺牲程序的清晰性和可读性来不必要地提高效率(效率不是第一位的)。
下面从三个方面进一步讨论效率问题。
- (1)程序运行时间
- (2)存储器效率
- (3) 输入输出的效率
7.2 软件测试基础
7.2.1 软件测试的目标
关于测试的一些规则:
- (1) 测试是为了发现程序中的错误而执行程序的过程;
- (2) 好的测试方案是极可能发现迄今为止尚未发现的错误的测试方案;
- (3) 成功的测试是发现了至今为止尚未发现的错误的测试。
由于测试的目标是暴露程序中的错误,从心理学角度看,由程序的编写者自己进行测试是不恰当的。因此,在综合测试阶段通常由其他人员组成测试小组来完成测试工作。
7.2.2 软件测试准则
为了能设计出有效的测试方案,主要的测试准则:
(1)所有测试都应该能追溯到用户需求。
正如上一小节讲过的,软件测试的目标是发现错误。从用户的角度看,最严重的错误是导致程序不能满足用户需求的那些错误。
(2)应该远在测试开始之前就制定出测试计划。
完成需求模型可着手制定测试计划,在建立了设计模型之后就可以立即开始设计详细的测试方案。
(3)Pareto原理
测试发现的错误中的80%很可能是由程序中20%的模块造成的。当然,问题是怎样找出这些可疑的模块并彻底地测试它们。
(4)应该从“小规模”“大规模”测试
首先重点测试单个程序模块,然后把测试重点转向在集成的模块簇中寻找错误,最后在整个系统中寻找错误。
(5)穷举测试是不可能的
所谓穷举测试就是把程序所有可能的执行路径都检查一遍的测试。此路不通!
因此,测试只能证明程序中有错误,不能证明程序中没有错误。但是,精心地设计测试方案,有可能充分覆盖程序逻辑并使程序达到所要求的可靠性。
(6)为了达到最佳的测试效果,应该由独立的第三方从事测试工作。
7.2.3 测试方法
测试有两种方法,即黑盒测试和白盒测试:
如果已经知道了产品应该具有的功能,可以通过测试来检验是否每个功能都能正常使用——黑盒测试;
如果知道产品的内部工作过程,可以通过测试来检验产品内部动作是否按照规格说明书的规定正常进行——白盒测试。
- 黑盒测试法把程序看作一个黑盒子,完全不考虑程序的内部结构和处理过程。黑盒测试又称为功能测试。
- 白盒测试法与黑盒测试法相反,它的前提是可以把程序看成装在一个透明的白盒子里,测试者完全知道程序的结构和处理算法。这种方法按照程序内部的逻辑测试程序,检测程序中的主要执行通路是否都能按预定要求正确工作。白盒测试又称为结构测试。
7.2.4 测试步骤
根据第4条测试准则,应该从“小规模”“大规模”测试,测试过程也必须分步骤进行。大型软件系统通常的测试过程基本上由下述几个步骤组成:
模块测试
模块测试的目的是保证每个模块作为一个单元能正确运行,所以模块测试通常又称为单元测试。
在这个测试步骤中所发现的往往是编码和详细设计的错误。
子系统测试
将若干经单元测试的模块放在一起形成一个子系统,进行的测试称为子系统测试。这个步骤着重测试模块的接口。
系统测试
将经过测试的子系统装配成一个完整的系统来的测试。
在这个过程中不仅应该发现设计和编码的错误,还应该验证系统确实能提供需求说明书中指定的功能,而且系统的动态特性也符合预定要求。在这个测试步骤中发现的往往是软件设计中的错误,也可能发现需求说明中的错误。
不论是子系统测试还是系统测试,都兼有检测和组装两重含义,通常称为集成测试。
验收测试
- 验收测试内容与系统测试基本类似,但是它是在用户积极参与下进行的,而且可能主要使用实际数据(系统将来要处理的信息)进行测试。
- 验收测试的目的是验证系统确实能够满足用户的需要,在这个测试步骤中发现的往往是系统需求说明书中的错误。验收测试也称为确认测试。
平行运行
平行运行就是同时运行新开发出来的系统和将被它取代的旧系统,以便比较新旧两个系统的处理结果。这样做的具体目的有如下几点:
(1)可以在准生产环境中运行新系统而又不冒风险;
(2)用户能有一段熟悉新系统的时间;
(3)以验证用户指南和使用手册之类的文档;
(4)能够以准生产模式对新系统进行全负荷测试,可以用测试结果验证性能指标。
7.2.5 测试阶段的信息流
如图所示描绘了测试阶段的信息流,输入信息有两类:
- (1)软件配置,包括需求说明书、设计说明书和源程序清单等;
- (2)测试配置,包括测试计划和测试方案(测试用例、预定要检验的功能以及预期的正确输出);
比较测试的实际结果和预期结果,若两者不一致则很可能是程序中有错误。
设法确定错误的准确位置并且改正它,这就是调试的任务。与测试不同,通常由程序的编写者负责调试。
如果出现要求修改设计的严重错误,软件的质量和可靠性是值得怀疑的,应该进一步仔细测试;
同上述相反,功能完成得很正常,遇到的错误也很容易改正,则仍然应该考虑两种可能:
- (1) 软件的可靠性是可以接受的;
- (2) 所进行的测试尚不足以发现严重的错误。
这些错误最终将被用户发现,而且需要在维护阶段改正它们(但是改正同一个错误需要付出的代价比在开发阶段高出许多倍)。
7.3 单元测试
单元测试:测试模块。
单元测试和编码属于软件过程的同一个阶段。
在编写出源程序代码并通过了编译程序的语法检查之后,就可以用详细设计作指南,对重要的执行通路进行测试,以便发现模块内部的错误。
可用人工测试和计算机测试两种测试方法,完成单元测试工作。
单元测试主要使用白盒测试技术,而且对多个模块的测试可以并行地进行。
7.3.1 测试重点
单元测试着重从5个方面对模块进行测试:
1. 模块接口
首先应该对通过模块接口的数据流进行测试,主要检查下述几个方面:
参数的数目、次序、属性或单位系统与变元是否一致;
是否修改了只作输入用的变元;
全局变量的定义和用法在各个模块中是否一致。
2. 局部数据结构
对于模块来说,局部数据结构是常见的错误来源。应该仔细设计测试方案,以便发现局部数据说明、初始化、默认值等方面的错误。
3. 重要的执行通路
由于通常不可能进行穷尽测试,因此,在单元测试期间选择最有代表性、最可能发现错误的执行通路进行测试就是十分关键的。
应该设计测试方案用来发现由于错误的计算、不正确的比较或不适当的控制流而造成的错误。
4. 出错处理通路
好的设计适当的处理错误的通路。
应该认真测试这种通路。当评价出错处理通路时,应该着重测试下述一些可能发生的错误:
(1) 对错误的描述是难以理解的;
(2) 记下的错误与实际遇到的错误不同;
(3) 在对错误进行处理之前,错误条件已经引起系统干预;
(4) 对错误的处理不正确;
(5) 描述错误的信息不足以帮助确定造成错误的位置。
5. 边界条件
边界测试是单元测试中最后的也可能是最重要的任务。
使用刚好小于、刚好等于和刚好大于最大值或最小值的数据结构、控制量和数据值的测试方案,非常可能发现软件中的错误。
7.3.2 代码审查
代码审查即人工测试源程序,由审查小组正式进行,它是一种非常有效的程序验证技术,对于典型的程序来说,可以查出30%~70%的逻辑设计错误和编码错误。审查小组最好由下述4人组成:
(1) 组长,应该是一个很有能力的程序员,而且没有直接参与这项工程;
(2) 程序的设计者;
(3) 程序的编写者;
(4) 程序的测试者。
审查之前,小组成员应该先研究设计说明书,力求理解这个设计。
一般先由设计者扼要地介绍他的设计,再由程序的编写者解释他是怎样用程序代码实现这个设计的,小组其他成员仔细倾听并力图发现其中的错误。
另外一项工作,是对照程序设计常见错误清单,分析审查这个程序。当发现错误时由组长记录下来,审查会继续进行(审查小组的任务是发现错误而不是改正错误)。
7.3.3 计算机测试
单元测试必须为每个单元测试开发驱动软件和(或)存根软件。
通常驱动程序也就是一个“主程序”,它接收测试数据,把这些数据传送给被测试的模块,并且印出有关的结果。
存根程序代替被测试的模块所调用的模块。因此存根程序也可以称为“虚拟子程序”。它使用被它代替的模块的接口,可能做最少量的数据操作,印出对入口的检验或操作结果,并且把控制归还给调用它的模块。
7.4 集成测试
集成测试是测试和组装软件的系统化技术,比如子系统测试,主要目标是发现与接口有关的问题。
由模块组装成程序时有两种方法:
非渐增式测试方法:
一种方法是先分别测试每个模块,再把所有模块按设计要求放在一起结合成所要的程序,这种方法;
渐增式测试方法:
是把下一个要测试的模块同已经测试好的那些模块结合起来进行测试,测试完以后再把下一个应该测试的模块结合进来测试。
这两种方法哪种更好一些呢?下面对比它们的主要优缺点:
- 非渐增式测试一下子把所有模块放在一起,极端困难,难于定位,难于改正。
- 渐增式测试比较容易定位和改正错误;对接口可以进行更彻底的测试;可以使用系统化的测试方法。
-
因此,目前在进行集成测试时普遍采用渐增式测试方法。
当使用渐增方式把模块结合到程序中去时,有自顶向下和自底向上两种集成策略。
7.4.1 自顶向下集成
自顶向下集成方法,从主控制模块开始,沿着程序的控制层次向下移动,逐渐把各个模块结合起来。
用深度优先的策略,或者使用宽度优先的策略。
深度优先,先组装在软件结构的一条主控制通路上的所有模块,选择主控制通路有一定的随意性。宽度优先,把处于同一个控制层次上的所有模块组装起来。
模块结合进软件结构具体过程有4个步骤:
1、对主控制模块进行测试,测试时用存根程序代替所有直接附属于主控制模块的模块;
2、根据选定的结合策略(深度优先或宽度优先),每次用一个实际模块代换一个存根程序(新结合进来的模块往往又需要新的存根程序);
3、在结合进一个模块的同时进行测试;
4、为了保证加入模块没有引进新的错误,可能需要进行回归测试(全部或部分重复做过的测试)。
从第二步开始不断地重复进行上述过程,直到构造起完整的软件结构为止。
7.4.2 自底向上集成
自底向上测试从“原子”模块(即在软件结构最低层的模块)开始组装和测试,具体步骤:
第一步,把低层模块组合成实现某个特定的软件子功能的族;
第二步,写一个驱动程序(用于测试的控制程序),协调测试数据的输入和输出;
第三步,对由模块组成的子功能族进行测试;
第四步,去掉驱动程序,沿软件结构自下向上移动,把子功能族组合起来形成更大的子功能族。
重复上述第二步到第四步。
7.5 确认测试
确认测试也称为验收测试,它的目标是验证软件的有效性。
那么,什么样的软件才是有效的呢?软件有效性的一个简单定义是: 如果软件的功能和性能如同用户所合理期待的那样,软件就是有效的。
需求分析阶段产生的软件需求规格说明书,准确地描述了用户对软件的合理期望,因此是软件有效性的标准,也是进行确认测试的基础。
确认测试必须有用户积极参与,或者以用户为主进行。
7.5.1 确认测试的范围
确认测试通常使用黑盒测试法。
应该仔细设计测试计划和测试过程,保证软件能满足所有功能要求,能达到每个性能要求,文档资料是准确而完整的,此外,还应该保证软件能满足如安全性、可移植性、兼容性和可维护性等的要求。
7.5.2 软件配置复查
确认测试的一个重要内容是复查软件配置。
复查的目的是保证软件配置的所有成分都齐全,质量符合要求,文档与程序完全一致,具有完成软件维护所必须的细节,而且已经编好目录。
7.5.3 Alpha和Beta测试
Alpha测试由用户在开发者的场所进行,并且在开发者对用户的“指导”下进行测试。开发者负责记录发现的错误和使用中遇到的问题。
总之,Alpha测试是在受控的环境中进行的。
Beta测试由软件的最终用户们在一个或多个客户场所进行,开发者通常不在Beta测试的现场。
用户记录在Beta测试过程中遇到的问题,并且定期把这些问题报告给开发者。
接收到在Beta测试期间报告的问题之后,开发者对软件产品进行必要的修改,并准备向全体客户发布最终的软件产品。
7.6 软件测试方法
软件测试方法分为两类:静态分析、动态测试。
7.6.1、静态分析方法
指以人工的、非形式化的方法对程序进行分析和测试。
桌前检查(Desk Checking):由程序员检查自己的程序,对源代码进行分析、检验。
代码会审(Code Reading Review):由程序员和测试员组成评审小组,按照“常见的错误清单”,进行会议讨论检查。
步行检查(Walkthroughs):与代码会审类似,也要进行代码评审,但评审过程主要采取人工执行程序的方式,故也称为“走查”。
7.6.2 动态测试方法(1)
通过选择适当的测试用例,执行程序。
常用的方法:
1、白盒法
分析程序的内部逻辑结构,注意选择适当的覆盖标准,设计测试用例,对主要路径进行尽可能多的测试。
2、黑盒法
不考虑程序的内部结构与特性,只根据程序功能或程序的外部特性设计测试用例。
7.6.2 动态测试方法(2)
1. 等价分类法
基本思想:根据程序的I/O特性,将程序的定义域划分为有限个等价区段 —“等价类”,从等价类中选择出的用例,具有“代表性”。
等价类分为:
有效等价类 — 对于程序的规格说明,是合理的、有意义的输入数据构成的集合。
无效等价类 —对于程序的规格说明,是不合理的、没有意义的输入数据构成的集合。
2. 边值分析法
基本思想: 选择等价类的边缘值作为测试用例,让每个等价类的边界都得到测试,选择测试用例既考虑输入亦考虑输出。
分析步骤:
A 先划分等价类。
B 选择测试用例,测试等价类边界。
边界选择原则:
A 按照输入值范围的边界。
B 按照输入/输出值个数的边界。
C 输出值域的边界。
D 输入/输出有序集的边界。