程序设计与调试
 

2009-03-03 来源:nuist.edu.cn

 

程序设计的主要依据是系统设计阶段的HIPO图以及数据库结构和编程代码设计。

程序调试的目的是要使用计算机语言实现系统设计中的每一个细节。

编程的目的是为了实现开发者在系统分析和系统设计中所提出的管理方法和处理构想,编程不是系统开发的目的。在编程实现时,建议尽量借用已有的程序和各种开发工具,尽快、尽好地实现系统。

一、程序设计的任务与基本要求

程序设计的任务是为新系统编写程序,即把详细设计的结果转换成某种计算机编程语言写成的程序。该阶段相当于机械工程中图纸设计完成的“制造”阶段,程序设计的好坏直接关系到能否有效地利用电子计算机来圆满地达到预期目的。

高质量的程序,必须符合以下基本要求:

1.程序的功能必须按照规定的要求,正确地满足预期的需要;

2.程序的内容清晰、明了、便于阅读和理解;

3.程序的结构严谨、简捷、算法和语句选用合理,执行速度快,节省机时;

4.程序和数据的存储、调用安排得当,节省存储空间;

5.程序的适应性强。程序交付使用后,若应用问题或外界环境有了变化时,调整和修改程序比较简便易行。

以上各要求并不是绝对的,允许根据系统本身以及用户环境的不同情况而有所侧重考虑。此外,程序设计结束后,还应写出操作说明书,说明执行该程序时的具体操作步骤。

一般说来,有了在详细设计阶段提供的详细设计方案,又有了高级编程语言,程序设计工作已经较为简单,因此本节不再讨论程序设计的具体细节。

二、程序设计方法

我们推荐使用现有软件工具的方法,这样做不但可以减轻开发的工作量,而且可以使系统开发过程规范,功能强,易于修改和维护。

1.结构化程序设计方法

结构化程序设计(STRUCTURED PROGRAMING,简称SP)方法,由E·DIJKSTRA等人于1972年提出,用于详细设计和程序设计阶段,指导人们用良好的思想方法,开发出正确又易于理解的程序。

鲍赫门(BOHM)和加柯皮(JACOPINI)在1966年就证明了结构定理:任何程序结构都可以用顺序、选择和循环这三种基本结构如图7-2-1(a)、(b)、(c)所示来表示。

a)顺序                    (b) 选择                (c) 循环

图7-2-1 程序的三种基本结构

结构化程序设计就建立在上述结构定理上,同时,DIJKSTRA主张取消GOTO语句,而仅仅用三种基本结构反复嵌套构造程序。

结构化程序设计至今还没有一个统一的定义,一般认为:结构化程序设计是一种设计程序的技术,它采用自顶向下逐步求精的设计方法和单入口单出口的控制技术。

按照这个思想,对于一个执行过程模糊不清的模块如图7-2-2(a)所示,可以采用以下几种方式对该过程进行分解:

(1) 用顺序方式对过程作分解,确定模糊过程中各个部分的执行顺序,如图7-2-2 (b)所示。

(2) 用选择方式对过程作分解,确定模糊过程中某个部分的条件,如图7-2-2 (c)所示。

(3) 用循环方式对过程作分解,确定模糊过程中主体部分进行重复的起始、终止条件,如图7-2-2 (d)所示。

对仍然模糊的部分可反复使用上述分解方法,最后即可使整个模块都清晰起来,从而把全部细节确定下来。

         (a)            (b)              (c)               (d)

图7-2-2 逐步求精的分解方法

由此可见,用结构化方法设计的结构是清晰的,有利于编写出结构良好的程序。因此开发人员必须用结构化程序设计的思想来指导程序设计的工作。

结构化程序设计的基本思想是按由顶向下逐步求精的方式,由三种标准控制结构反复嵌套来构造一个程序。按照这种思想,可以对一个执行过程模糊不清的模块,以顺序、选择、循环的形式加以分解,最后使整个模块都清晰起来,从而确定全部细节。

用结构化程序设计方法逐层把系统划分为大小适当、功能明确、具有一定独立性、并容易实现的模块,从而把一个复杂的系统的设计转变为多个简单模块的设计。用结构化程序设计方法产生的程序也由许多模块组成,每个模块只有一个入口和一个出口,程序中一般没有GOTO语句,所以把这种程序称为结构化程序。结构化程序易于阅读,而且可提高系统的可修改性和可维护性。

由于大多高级语言都支持结构化程序设计方法,其语法上都含有表示三种基本结构的语句,所以用结构化程序设计方法设计的模块结构到程序的实现是直接转换的,只需用相应的语句结构代替标准的控制结构即可,因此减轻了程序设计的工作量。

2.速成原型式的程序开发方法

这种开发方法是 :

首先将HIPO图中类似带有普遍性的功能模块集中,如菜单模块、报表模块、查询模块、统计分析和图形模块等。

寻找有无相应和可用的软件工具,若有则使用这些工具生成这些程序模型原型。否则,可考虑开发一个能够适合各子系统情况的通用模块。

3.面向对象程序设计方法

面向对象的程序设计方法一般应与OOD所设计的内容相对应。它实际上是一个简单、直接的映射过程,即将OOD中所定义的范式直接用面向对象的程序(OOP),如C++,Smalltalk,Visual C等来取代即可。

三、程序设计语言的选择

在程序设计之前,从系统开发的角度考虑选用哪种语言来编程是很重要的。一种合适的程序设计语言能使根据设计去完成编程时困难最少,可以减少所需要的程序调试量,并且可以得出更容易阅读和维护的程序。

汇编语言虽然占主存容量少且运行速度快,但是程序设计即困难又容易出错。随着计算机应用深入发展,管理信息系统的程序规模日益增大,采用的程序设计语言也逐渐发生变化,一般不用汇编语言,而采用高级语言。选择适合于管理信息系统的程序设计语言应该从以下几个方面考虑:

1. 语言的结构化机制与数据管理能力

选用高级语言应该有理想的模块化机制、可读性好的控制结构和数据结构,同时具备较强的数据管理能力,例如数据库语言。

2. 语言可提供的交互功能

选用的语言必须能够提供开发、美观的人机交互程序的功能,例如色彩、音响、窗口等。这对用户来说是非常重要的。

3. 有较丰富的软件工具

如果某种语言支持程序开发的软件工具可以利用,则使系统的实现和调试都变得比较容易。

4. 开发人员的熟练程度

虽然对于有经验的程序员来说,学习一种新语言并不困难,但要完全掌握一种新语言并用它编出高质量的程序来,却需要经过一段时间的实践。因此,如果可能的话,应该尽量选择一种已经为程序员所熟悉的语言。

5. 软件可移植性要求

如果开发出的系统软件将在不同的计算机上运行,或打算在某个部门推广使用,那么应该选择一种通用性强的语言。

6. 系统用户的要求

如果所开发的系统由用户负责维护,用户通常要求用他们熟悉的语言书写程序。

四、程序设计的风格

程序的可读性对于软件,尤其是对软件的质量有重要影响,因此在程序设计过程中应当充分重视。为了提高程序的可读性,在程序设计风格方面应注意以下几点:

1.适当的程序注释

程序中适当地加上注释后,可以使程序成为一篇“自我解释”的文章,读程序时就不必翻阅其它说明材料了。

注释原则上可以出现在程序中的任何位置,但是如果使注释和程序的结构配合起来则效果更好。注释一般分为两类:序言性注释和描述性注释。

序言性注释出现在模块的首部,内容包括:模块功能说明;界面描述(如调用语句格式、所有参数的解释和该模块需调用的模块名等);某些重要变量的使用、限制;开发信息如作者、复查日期、修改日期等。

描述性注释嵌在程序之中,用来说明程序段的功能或数据的状态。

如果详细设计是用过程设计语言(PDL)描述的,则编程时可将PDL描述嵌在程序中。

书写注释时应注意:

(1)注释应和程序一致,修改程序时应同时修改注释,否则会起反作用,使人更难明白。

(2)注释应提供一些程序本身难以表达的信息。

(3)为了方便用户今后维护,注释应尽量多用汉字。

2.有规律的程序书写格式

恰当的书写格式将有助于阅读,在结构化程序设计中一般采用所谓“缩排法”来写程序,即把同一层次的语句行左端对齐,而下一层的语句则向右边缩进若干格书写,它能体现程序逻辑结构的深度。此外,在程序段与段之间安排空白行,也有助于阅读。

3.恰当选择变量名

理解程序中每个变量的含义是理解程序的关键,所以变量的名字应该适当选取,使其直观,易于理解和记忆。例如采用有实际意义的变量名、 不用过于相似的变量名、 同一变量名不要具有多种意义。此外,在编程前最好能对变量名的选取约定统一标准,以后阅读理解就会方便的多。

五、管理信息系统的基本程序模块

一个管理信息系统的软件由很多程序模块组成,这些程序模块可以归纳成为几种基本类型,包括控制模块、输入及校验模块、修改或更新模块、分类合并模块、计算模块、查询、检索模块、输出模块和预测、优化模块等,其结构如图7-2-3所示。

基本程序模块结构图片

基本程序模块结构动画

1.控制模块

控制模块包括主控制模块和各级控制模块。控制模块的主要功能是根据用户要求信息,由用户确定处理顺序,然后控制转向各处理模块的入口。

2.输入模块

输入模块主要用来输入数据。输入方式有直接用键盘输入和软盘输入两种。

3.输入数据校验模块

该模块对已经输入计算机中的数据进行校验,以保证原始数据的正确性。校验的方法通常有重复输入校验和程序校验两种。

4.输出模块

输出模块用来将计算机的运行结果通过屏幕、打印机或磁盘、磁带等设备输出给用户。在管理信息系统中,一般都采用大量的表格、图表需要输出,因此输出模块的质量直接关系到整个系统的性能。

5.处理模块

根据管理信息系统的不同应用部门和要求,有不同的处理功能,通常有以下几种类型。

(1)文件更新模块程序

当系统应用的数据发生变化时,需要修改数据文件。例如,增加新的记录,修改数据项或记录,删除某些不需要的记录等。

一般来说,文件更新模块应该具有下述功能:

对记录中关键字的控制功能,通过关键字查找相应记录;

控制总记录数的功能,以便控制追加、插入记录的位置;

具有记录地址或字节位置的控制功能,以便确定修改数据的位置,控制插入或者追加的数据位置。

(2)分类合并程序

分类合并程序的主要功能是对已经建立的文件,按某关键字进行分类合并。例如,在材料核算系统中耗用材料要按照材料类型合并处理。分类合并程序应该具有下述功能:

具有控制记录总数的功能;

具有字符串比较的功能;

具有排序、统计和计数功能。

(3)计算程序

进行计算机处理,包括同类记录中各数据项的运算。例如,将材料单价与数量相乘,求得某材料的应付金额;若要计算某种材料在某个产品中的总消耗量,则必须累计各次材料领用量,然后减去废料量。

(4)数据检索程序

是为用户提供查询有关信息的程序,它包括输入查询要求和输出特定的查询结果。它是管理信息系统的人机接口,对于人机交互的友好程序以及查询响应时间等均有较高要求。

(5)预测或优化程序

使用预测或优化的数学模型,利用管理信息系统所提供的有关数据,进行计算和分析并输出结果,用来辅助企业或部门的管理人员进行决策。例如库存管理中的ABC分类、最佳订货量计算,财务管理中的资金分析等。

一个完整的计算机管理信息系统,实质上是以上各类基本程序模块的组合体。

六、衡量编程工作指标

从目前的技术发展来看,衡量编程工作的指标大致可分为5个方面:

  • 可靠性(Reliability):它可分解为两个方面的内容:一是程序或系统的安全可靠性,如数据存取的安全可靠性,通讯的安全可靠性,操作权限的安全可靠性。另一个方面是程序运行的可靠性,这一点只能靠程序调试时严格把关来保证编程工作质量。
  • 实用性(Suability) :一般从用户的角度来审查,它是指系统各部分是否都非常方便实用。它是系统今后能否投入实际运行的重要保证。
  • 规范性(Standardability):即系统的划分、书写格式、变量的命名等等都是按统一规范进行的。这对于今后程序的阅读、修改和维护都是十分必要的。
  • 可读性(Readability):即程序的清晰,没有太多繁杂的技巧,能够使他人容易读懂。它对于大规模过程化开发软件非常重要。
  • 可维护性(maintainability):即程序各部分相互独立,没有调用子程序以外的其它数据关联。也就是说不会发生那种在维护时,牵一发动全身的连锁反应。

一般一个规范性、可读性、结构划分都很好的程序模块,它的可维护性也是比较好的。

七、常用的编程工具

目前市场上能够提供系统选用的编程工具十分丰富。它们不仅在数量和功能上突飞猛进,而且在内涵的拓展上也日新月异,为我们开发系统提供了越来越多、越来越方便的实用手段。

一般比较流行的软件工具开分为6类:一般编程语言,数据库系统,程序生成工具、专用系统开发工具、客户/服务器(client/Server, C/S)型工具以及面向对象的编程工具。

1.常用编程语言类

它是指由传统编程工具发展而来的一类程序设计语言。通常有:C语言、C++语言、COBOL语言、PL/1语言、PROLOG语言、OPS语言等等。

这些语言一般不具有很强的针对性,它只是提供了一般程序设计命令的基本集合,因而适应范围很广,原则上任何模块都可以用它们来编写。

缺点:其适应范围广是以用户编程的复杂程度为代价的,程序设计的工作量很大。

2.数据库类

它是信息系统中数据存放的中心和整个系统数据传递和交换的枢纽。目前市场上提供的主要有两类:xBASE系统(以微机关系数据库为基础)和大型数据库系统。

xBASE系统:它主要是指以微机为基础所形成的关系数据库及其程序开发语言。典型产品代表有:dBASE-II、III、IV,FoxBASE以及FoxPro等各种版本。

大型数据库系统:指规模较大、功能较齐全的大型数据库系统。

目前较为典型的系统有:ORACLE系统,SYBASE统,INGRES系统,INFORMAX系统,DB2系统等等。

这类系统的最大特点是功能齐全,容量巨大,适合于大型综合类数据库系统的开发。在使用时配有专门的接口语言,可以允许各类常用的程序语言(称之为主语言)任意地访问数据库内的数据。

3.程序生成工具类

它是指第四代程序(4GLs)生成语言,是一种常用数据处理功能和程序之间的对应关系的自动编程工具。

较为典型的产品有:AB(Application Builder应用系统建造工具),屏幕生成工具、报表生成工具以及综合程序生成工具,即有FoxPro, Visual BASIC, Visual C++, CASE, Power Builder等。

目前这类工具发展的一个趋势是功能大型综合化,生成程序模块语言专一化。

4.系统开发工具类

它是在程序生成工具基础上进一步发展起来的,它不但具有4GLs的各种功能,而且更加综合化、图形化,使用起来更加方便。

目前主要有两类:专用开发工具类和综合开发工具类。

专用开发工具类:是指对某应用领域和待开发功能针对性都较强的一类系统开发工具。

综合开发工具类:它是指一般应用系统和数据处理功能的一类系统开发工具。其特点是可以最大限度地适用于一般应用系统开发和生成。

如专门用于开发查询模块的SQL,专门用于开发数据处理模块的SDK(Structured Development Kits),专门用于人工智能和符号处理的Prolog for Windows, 专门用于开发产生式规则知识处理系统的OPS(Operation Process System)等等。

在实际开发系统时,只要我们再自己动手将特殊数据处理过程编制成程序模块,则可实现整个系统。

常见的系统开发工具有:FoxPro, dBASE-V, Visual BASIC, Visual C++, CASE, Team Enterprise Developer等等。

这种工具虽然不能帮用户生成一个完整的应用系统,但可帮助用户生成应用系统中大部分常用的处理功能。

5.客户/服务器(C/S)工具类

它是采用了人类在经济和管理学中经常提到的“ 专业化分工协作”的思想而产生的开发工具。

它是在原有开发工具的基础上,将原有工具改变为一个 个既可被其它工具调用的,又可以调用其它工具的“公共模块”。

在整个系统结构方面,这类工具采用了传统分布式系统的思想,产生了前台和后台的作业方式,减轻了网络的压力,提高了系统运行的效率。

常用的C/S工具有:

FoxPro, Visual BASIC, Visual C++, Excel, Powerpoint, Word, Delphi C/S, Power Builder Enterprise, Team Enterprise Developer等等。

这类工具的特点是它们之间相互调用的随意性。例如在FoxPro中通过DDE(Dynamic Data Exchange, 动态数据交换)或OLE(Object Linking and Embedding,对象的链接和嵌入)或直接调用Excel, 这时FoxPro应用程序模块是客户,Excel应用程序是服务器。

6.面向对象编程工具类

它主要是指与OO方法相对应的编程工具。目前常见的工具有:C++(或VC++),Smalltalk。这一类针对性较强,且很有潜力,其特点是必须与整个OO方法相结合。

八、程序调试

1.调试的意义和目的

在管理信息系统的开发过程中,面对着错综复杂的各种问题,人的主观认识不可能完全符合客观现实,开发人员之间的思想交流也不可能十分完善。所以,在管理信息系统开发周期的各个阶段都不可避免地会出现差错。开发人员应力求在每个阶段结束之前进行认真、严格的技术审查,尽可能早的发现并纠正错误,否则等到系统投入运行后再回头来改正错误将在人力、物力上造成很大的浪费,有时甚至导致整个系统的瘫痪。然而,经验表明,单凭审查并不能发现全部差错,加之在程序设计阶段也不可避免还会产生新的错误,所以,对系统进行调试是不可缺少的,是保证系统质量的关键步骤。统计资料表明,对于一些较大规模的系统来说,系统调试的工作量往往占程序系统编制开发总工作量的40%以上。

调试的目的在于发现其中的错误并及时纠正,所以在调试时应想方设法使程序的各个部分都投入运行,力图找出所有错误。错误多少与程序质量有关。即使这样,调试通过也不能证明系统绝对无误,只不过说明各模块、各子系统的功能和运行情况正常,相互之间连接无误,系统交付用户使用以后,在系统的维护阶段仍有可能发现少量错误并进行纠正,这也是正常的。

2.调试的策略和基本原则

先看一个例子。

例:图7-2-4所示的是一个小程序的控制流程图,该程序由一个循环语句组成,循环次数可达20次,循环体中是一组嵌套的IF语句 ,其可能的路径有五条,所以从程序的入口A到出口B的路径数高达520≈1014。如果编写一个调试例子,并用它来调试这个程序的一条路径要花一分钟,则调试每一条路径就需要二亿年。

图7-2-4 控制流程图示例

 

这个例子说明,要想通过“彻底”地调试找出系统的全部错误是不可能的。因此,调试阶段要考虑的基本问题就是“经济性”了。调试采取的策略是:在一定的开发时间和经费的限制下,通过进行有限步操作或执行调试用例,尽可能多发现一些错误。

调试阶段还应注意以下一些基本原则:

(1)调试用例应该由“输入数据”和“预期的输出结果”组成。这就是说,在执行程序之前应该对期望的输出有很明确的描述,调试后可将程序的输出同它仔细对照检查。若不事先确定预期的输出,这可能把似乎是正确而实际是错误的结果当成是正确结果。

(2)不仅要选用合理的输入数据进行调试,还应选用不合理的甚至错误的输入数据。许多人往往只注意前者而忽略了后一种情况,为了提高程序的可靠性,应认真组织一些异常数据进行调试,并仔细观察和分析系统的反应。

(3)除了检查程序是否做了它应该做的工作,还应检查程序是否做了它不该做的事情。

例如除了检查工资管理程序是否为每个职工正确地产生了一份工资单以外,还应检查它是否还产生了多余的工资单。

(4)应该长期保留所有的调试用例,直至该系统被废弃不用为止。

在管理信息系统的调试中,设计调试用例是很费时的,如果将用过的例子丢弃了,以后一旦需要再调试有关的部分时(例如技术鉴定系统维护等场合)就需要再花很多人工。通常,人们往往懒得再次认真地设计调试用例,因而下次调试时很少有初次那样全面。如果将所有调试用例作为系统的一部分保存下来,就可以避免这种情况的发生。

3.测试方法

测试包括三方面,即设计“测试用例”,执行被测程序和分析执行结果并发现错误。设计测试用例是开始程序测试的第一步,也是有效地完成测试工作的关键。按照在设计测试用例时是否涉及程序的内部结构,可以分为白盒测试和黑盒测试两种方法。

白盒测试时,测试者对被测试程序的内部结构是清楚的。他从程序的逻辑结构人手,按照一定的原则来设计测试用例,设定测试数据。由于被测程序的结构对测试者是透明的,因此有些书本又称这类测试为玻璃盒测试或结构测试。

黑盒测试的情况正好相反。此时,测试者把被测程序看成一个黑盒,完全用不着关心程序的内部结构。设计测试用例时,仅以程序的外部功能为根据。一方面检查程序能否完成一切应做的事情,另一方面要考察它能否拒绝一切不应该做的事情。由于黑盒测试着重于检查程序的功能,所以也称为功能测试。

(1)设计测试用例的基本目标

设计测试用例是测试阶段的关键技术问题。所谓测试用例就是以发现程序错误为目的而精心设计的一组测试数据,包括预定要测试的功能,应该输入的测试数据和预期的结果。可以写成:

测试用例={输入数据+期望结果}

设计测试用例最困难的问题是设计测试的输入数据。不同的测试数据发现程序错误的能力差别很大,为了提高测试效果、降低测试成本,应该选用高效的测试数据。因为不可能进行穷尽的测试,选用少量“最有效的”测试数据,做到尽可能完备的测试就很重要了。因此,设计测试用例的基本目标就是确定一组最可能发现多个错误或多类错误的测试数据。

(2)设计测试数据的技术

已经研究出许多设计测试数据的技术,这些技术各有优缺点,没有哪一种是最好的,更没有一种可以代替其余所有技术;同一种技术在不同应用场合效果可能相差很大,因此,通常需要联合使用多种测试数据。

本书介绍的设计测试数据技术主要有:适用于黑盒测试的等价划分、边界值分析及错误推测法等;适用于白盒测试的逻辑覆盖法等。

通常设计测试数据的做法是:用黑盒法设计基本的测试用例,再用白盒法补充一些方案。

(3)黑盒测试技术

<1>等价划分

等价划分是黑箱测试的一种技术。前面讲过,穷尽的黑盒测试需要使用所有有效的和无效的输入数据来测试程序,通常这是不现实的。因此,只能选取少量有代表性的输入数据,以期用较小的代价暴露出较多的程序错误。

这种方法是把被测试的程序的所有可能的输入数据(有效的和无效的)划分成若干个等价类,把无限的随机测试变成有针对性的等价类测试。按这种方法可以合理地做出下列假定:每类中的一个典型值在测试中的作用与这一类中所有其它值的作用相同。因此,可以从每个等价类中只取一组数据作为测试数据。这样可选取少量有“代表性”的测试数据,来代替大量相类似的测试,从而大大减少总的测试次数。

设计等价类的测试用例一般分为两步进行:

第一步:划分等价类并给出定义;

第二步:选择测试用例。

选择的原则是:有效等价类的测试用例尽量公用,以期进一步减少测试的次数;无效等价类必须每类一例,以防漏掉本来可能发现的错误。

划分等价类时,需要研究程序的功能说明,以确定输入数据的有效等价类和无效等价类。在确定输入数据的等价类时常常还需要分析输出数据的等价类,以便根据输出数据的等价类导出对应的输入数据等价类。

划分等价类需要经验,下述几条启发式规则可能有助于等价类的划分:

? 如果规定了输入值的范围,则可划分出一个有效的等价类(输入值在此范围内),两个无效的等价类(输入值小于最小值和大于最大值);

? 如果规定了输入数据的个数,则类似地可以划分出一个有效的等价类和两个无效的等价类;

? 如果规定了输入数据的一组值,而且程序对不同输入值做不同处理,则每个允许的输入值是一个有效的等价类,此外还有一个无效的等价类(任一个不允许的输入值);

? 如果规定了输入数据必须遵循的规则,则可以划分出一个有效的等价类(符合规则)和若干无效的等价类(从各种不同角度违反规则);

? 如果规定了输入数据为整型,则可以划分出正整数、零和负整数等三个有效类;

? 如果程序的处理对象是表格,则应该使用空表,以及一项或多项的表。

以上列出的启发式规则只是测试时可能遇到的情况中的很小的一部分,实际情况千变万化,根本无法一一列出。为了正确划分等价类,一是要注意积累经验,二是要正确分析被测程序的功能。此外,在划分无效等价类时,还必须考虑编译程序的检错功能,一般说来,不需要设计测试数据用来暴露编译程序肯定能发现的错误。最后说明一点,上面列出的启发式规则虽然都是针对输入数据说的,但是其中绝大部分也同样适用于输出数据。

划分初等价类以后,根据等价类设计测试用例时主要使用下面两个步骤:

① 设计一个新的测试用例以尽可能多地覆盖尚未覆盖的有效等价类,重复这一步骤直到所有有效等价类都被覆盖为止;

② 设计一个新的测试用例,使它覆盖一个而且只覆盖一个尚未覆盖的无效等价类,重复这一步骤直到所有无效等价类都被覆盖为止;

注意,通常程序发现一类错误后就不再检查是否还有其它错误,因此,应该使每个测试用例只覆盖一个无效等价类。下面举例说明。

等价划分的例子

例:某城市的电话号码由三部分组成。这三部分的名称和内容分别是

地区码:空白或三位数字;

前 缀:非’0’或’1’开头的三位数;

后 缀:四位数字。

假定被调试的程序能接受一切符合上述规定的电话号码,拒绝所有不符合规定的号码,就可用等价分类法来设计它的调试用例。

解:第一步:划分等价类,包括4个有效等价类,11个无效等价类。表7-2-1列出了划分的结果。在每一等价类之后加有编号,以便识别。

表7-2-1 电话号码程序的等价划分

输入条件

有效等价类

无效等价类

地区码

空白(1),3位数字(2)

有非数字字符(5),少于3位数字(6),多于三位数字(7)

前缀

从200到999之间的3位数字(3)

有非数字字符(8),起始位为"0"(9),起始位为"1"(10),少于3位数字(11),多于3位数字(12)

后缀

4位数字(4)

有非数字字符(13),少于4位数字(14),多于4位数字(15)

第二步:确定调试用例。表7-2-1中有4个有效等价类,可以公用以下两个次数用例:

调试数据

范围

期望结果

(   )  2762345

等价类(1)(3)(4)

有效

(635)  8059321

等价类(2)(3)(4)

有效

对11个无效等价类,要选择11个调试用例,如下所示:

 调试数据

范围

 期望结果

(20A)  1234567   

无效等价类(5)

无效

(33 )  2345678  

无效等价类(6)

无效

(7777) 3456789   

无效等价类(7) 

无效

(777)  34A6789

无效等价类(8) 

无效

(234)  0456789

无效等价类(9)

无效

(777)  1456789

无效等价类(10) 

无效

(777)  346789  

无效等价类(11)

无效

(777)  23456789   

无效等价类(12)

无效

(777)  345678A 

无效等价类(13) 

无效

(777)  345678 

无效等价类(14)

无效

(777)  34556789 

无效等价类(15) 

无效

选取的调试数据可以不同,关键是与调试内容相符。

<2>边界值分析

经验表明,处理边界情况时程序最容易发生错误。例如,许多程序错误出现在下标、纯量、数据结构和循环等等的边界附近。因此,设计使程序运行在边界情况附近的测试方案,暴露出错误的可能性更大一些。

使用边界值分析方法设计测试用例首先应该确定边界情况,这需要经验和创造性,通常输入等价类和输出等价类的边界,就是应该着重测试的程序边界情况。选取的测试数据应该刚好等于、刚刚小于和刚刚大于边界值。也就是说,按照边界值分析法,应该选取刚好等于、稍小于和稍大于等价类边界值的数据作为测试数据,而不是选取每个等价类内的典型值作为测试数据。

通常设计测试用例时总是联合使用等价划分和边界值分析两种技术。例如,税法规定个人的收入所得税从超过800元开始征收。如果用一个程序来计算税款,则“收入≤800”就是一个判定条件,满足条件的人免税,否则对超出800元的部分征税。在选择测试用例时,可以用300、900两个测试数据分别代表免税和征税两个等价类,还可以就800这个边界值作为测试数据。

<3>错误推测

使用边界分析法和等价划分技术,可以帮助开发人员设计具有代表性的,容易暴露程序错误的测试用例。但是,不同类型不同特点的程序通常又有一些特殊的容易出错的情况。此外,有时分别使用每组测试数据时程序都能正常工作,这些输入数据的组合却可能检测出程序的错误。一般说来,即使是一个比较小的程序,可能的输入组合数也往往十分巨大,因此必须依靠测试人员的经验和直觉,从各种可能的测试用例中选出一些最可能引起程序出错的方案。对于程序中可能存在哪类错误的推测,是挑选测试用例时的一个重要因素。

错误推测法在很大程度上靠直觉和经验进行。它的基本想法是列举出程序中可能有的错误和容易发生错误的特殊情况,并且根据它们选择测试用例。对于程序中容易出错的情况也有一些经验总结出来,例如,输入数据为零或输出数据为零往往容易发生错误;如果输入或输出的数目允许变化(例如,被检索的或生成的表的项数),则输入或输出的数目为0和1的情况(例如,表为空或只有一项)是容易出错的情况。还应该仔细分析程序规格说明书,注意找出其中遗漏或省略的部分,以便设计相应的测试用例,检测程序员对这些部分的处理是否正确。

例如,当对一个排序程序进行测试时,可先用边界值分析法设计测试用例:

输入表为空表;

输入表中仅有一个数据;

输入表为满表。

再用错误推测法补充一些例子:

输入表已经排好了序:

输入表的排序恰与所要求的顺序相反(如程序功能为由小到大排序,输入表为由大到小排序);

输入表中的所有数据全部相同。

此外,经验说明,在一段程序中已经发现的错误数目往往和尚未发现的错误数目成正比。因此,在进一步测试时要着重测试那些已发现较多错误的程序段。

<4>输入组合

等价划分法和边界值分析法都只孤立地考虑各个输入数据的测试功效,而没有考虑多个输入数据的组合效应,可能会遗漏了输入数据易于出错的组合情况。选择输入组合的一个有效途径是利用判别表和判定树为工具,列出输入数据各种组合与程序应作的动作(及相应的输出结果)之间的对应关系,然后为判定表的每一列至少设计一个测试用例。

选择输入组合的另一个有效途径是把计算机测试和人工检查代码结合起来。例如,通过代码检查程序中两个模块使用并修改某些共享的变量,如果一个模块对这些变量的修改不正确,则会引起模块出错,因此这是程序发生错误的一个可能的原因。应该设计测试用例,在程序的一次运行中同时检测这两个模块,特别要着重检测一个模块修改了共享变量后另一个模块能否象预期的那样正常使用这些变量。反之,如果两个模块相互独立,则没有必要测试它们的输入组和情况。通过代码检查也能发现模块相互依赖的关系,在这种情况下,不仅必须测试这个转换函数,还应该测试调用它的算术函数在转换函数接受到无效输入时的响应。

(4)逻辑覆盖(白箱测试技术)

有选择的执行程序中某些最有代表性的通路是对穷尽测试的唯一可行的替代方案。所谓逻辑覆盖是对一系列测试过程的总称,这组测试过程逐渐进行越来越完整的通路测试。

测试数据执行(或叫覆盖)程序逻辑的程度可以划分成哪些不同的等级呢?从覆盖源程序的语句的详尽程度分析,大致有以下一些不同的覆盖标准:

<1>语句覆盖

为了暴露程序中的错误,至少每个语句应该执行一次。语句覆盖的含义是,选择足够多的测试数据,使被测试程序中的每个语句至少执行一次。

例如: 图7-2-5是一个被测模块的流程图,

图7-2-5 被测模块的流程图

它的源程序(用PASCAL书写)如下:

PROCEDURE  EXAMPLE (AB:REAL; VAR X :REAL)

         BEGIN

             IF (A>1)AND (B=0)

                 THEN X:=X/A

             IF (A=2) OR  (X>1)

                 THEN X:=X+1

        END;

为了使每个语句都执行一次,程序的执行路径应该是sacbed,为此只需要输入下面的测试数据(实际上X可以是任意实数),

  A=2,B=0,X=4

语句覆盖对程序的逻辑覆盖很少,在例子中两个判定条件都只测试了图6-5被测模块的流程图上面的条件为真的情况,如果条件为假时处理有错误,显然不能发现。此外,语句覆盖只关心判定表达式的值,而没有分别测试判定表达式中每个条件取不同值时的情况。在上面的例子中,为了执行sacbed路径,以测试每个语句,只需两个判定表达式(A>1)AND (B=0)和(A=2) OR (X>1)都取真值,因此使用上述一组测试数据就够了。但是,如果程序中把第一个判定表达式中的逻辑运算符“AND”错写成“OR”,或把第二个判定式中的条件“X>1”误写成“X<1”,使用上面的测试数据并不能查出这些错误。

综上所述,可以看出语句覆盖是很弱的逻辑覆盖标准,为了更充分的测试程序,可以采用下述的逻辑覆盖标准。

<2>判定覆盖

判定覆盖的含义是,不仅每个语句必须至少执行一次,而且每个判定的可能的结果都应该至少执行一次,也就是每个判定的每个分支都至少执行一次。

对于上述例子来说,能够分别覆盖路径sacbed和sabd的两组测试数据,或者可以分别覆盖路径sacbd和sabed的两组测试数据,都满足判定覆盖标准。例如,用下面两组测试数据就可以做到判定覆盖:

① A=3,B=0,X=3 (覆盖sacbd)

② A=2,B=1,X=1 (覆盖sabed)

判定覆盖比语句覆盖强,但是对程序逻辑的覆盖程度仍然不高,例如,上面的测试数据只覆盖了程序全部路径的一半。

<3>条件覆盖

条件覆盖的含义是,不仅每个语句至少执行一次,而且是判定表达式中的每个条件都取到各种可能的结果。

图7-2-5的例子中共有两个判定表达式,每个表达式中有两个条件,为了做到条件覆盖,应该选取测试数据使得在a点有下述各种结果出现:

A> 1, A≤1, B=0, B≠0

在b点有下述各种结果出现:

A=2, A≠2, X>1, X≤1

只需要使用下面两组测试数据就可以达到上述覆盖标准:

① A=2, B=0, X=4

(满足A>1, B=0, A=2和X>1的条件,执行路径sacbed)

② A=1, B=1, X=1

(满足A≤1, B≠0, A≠2和X≤1的条件,执行路径sabd)

条件覆盖通常比判定覆盖强,因为它使判定表达式中每个条件都取到了两个不同的结果,判定覆盖却只关心整个判定表达式的值。例如,上面两组测试数据也同时满足判定覆盖标准。但是,也可能有相反的情况,虽然每个条件都取到了两个不同的结果,判定表达式却始终只取一个值。例如,如果使用下面两组测试数据,则只满足条件覆盖标准并不满足判定覆盖标准(第二个判定表达式的值总为真):

① A=2, B=0, X=1

(满足A>1, B=0, A=2和X≤1的条件,执行路径sacbed)

② A=1, B=1, X=2

(满足A≤1, B≠0, A≠2和X>1的条件,执行路径sabed) 

<4>判定/条件覆盖

既然判定覆盖不一定包含条件覆盖,条件覆盖也不一定包含判定覆盖,自然会提出一种能同时满足这两种覆盖标准的逻辑覆盖,这就是判定/条件覆盖,它的含义是,选取足够多的测试数据,使得判定表达式中的每个条件都取到各种可能的值,而且每个判定表达式也都取到各种可能的结果。

对于图7-2-5例子而言,下述两组测试数据满足判定/条件覆盖标准:

I. A=2, B=0,X=4

II. A=1, B=2,X=1

但是,这两组测试数据也就是为了满足条件覆盖标准最初选取的两组数据,因此,有时判定/条件覆盖也并不比条件覆盖更强。

<5>条件组合覆盖

条件组合覆盖是更强的逻辑覆盖标准,它要求选取足够多的测试数据,使得每个判定表达式中条件的各种可能组合都至少出现一次。对于图7-2-5的例子,共有八种可能的条件合,它们是:

① A>1, B= 0

②A>1, B≠0

③A≤1,B= 0

④A≤1,B≠0

⑤A=2, X>1

⑥A=2, X≤1

⑦A≠2,X>1

⑧A≠2,X≤1

和其它逻辑覆盖标准中的测试数据一样,条件组合(5)-(8)中的X值是指在程序流程图第二个判定框(b点)的X值。

下面的四组测试数据可以使上面列出的八种组合每种至少出现一次:

① A=2,B=0,X=4

(针对1,5两种组合,执行路径sacbed)

②A=2,B=1,X=1

(针对2,6两种组合,执行路径sabed)

③A=1,B=0,X=2

(针对3,7两种组合,执行路径sabed)

④ A=1,B=1,X=1

(针对4,8两种组合,执行路径sabd)

显然,满足条件组合覆盖标准的测试数据,也一定满足判定覆盖、条件覆盖和判定/条件覆盖标准。因此,条件组合覆盖是前述几种覆盖标准中最强的。但是,满足条件组合覆盖标准的测试数据并不一定能使程序中的每条路径都执行到,例如,上述四组测试数据都没有测试到路径sacbd。

以上根据测试数据对源程序语句检测的详尽程序,简单讨论了几种逻辑覆盖标准。在上面的分析过程中常常谈到测试数据执行的程序路径,显然,测试数据可以检测的程序路径的多少,也反映了对程序测试的详尽程度。

(5)设计测试用例小结

以上简单介绍了设计测试用例的几种基本方法,使用每种方法都能设计出一组有用的测试用例,但是没有一种方法能设计出全部测试用例。此外,不同的方法各有所长,用一种方法设计出的测试用例可能最容易发现某些类型的错误,对另外一些类型的错误可能不易发现。

因此,对软件系统进行实际测试时,应该联合使用各种设计测试用例的方法,形成一种综合策略。通常的做法是,用黑盒法设计基本的测试用例,再用白盒法补充一些必要的测试用例。具体地说,可以使用下述策略结合各种方法:

<1> 在任何情况下都应该使用边界值分析的方法。经验表明,用这种设计方法设计出的测试用例暴露程序错误的能力最强。注意,应该既包括输入数据的边界情况又包括输出数据的边界情况。

<2> 必要时用等价划分法补充测试用例。

<3> 必要时再用错误推测法补充测试用例。

<4> 对照程序逻辑,检查已经设计出的测试用例。可以根据对程序可靠性的要求采用不同的逻辑覆盖标准,如果现有测试用例的逻辑覆盖程度没有达到要求的覆盖标准,则应再补充一些测试用例。

应该强调指出,即使使用上述综合策略设计测试用例,仍然不能保证测试将发现一切程序错误;但是,这个策略确实是在测试成本和测试效果之间的一个合理的折衷。通过前面的叙述可以看出,软件测试确实是一件十分艰巨繁重的工作。

4.调试步骤

一个管理信息系统通常由若干子系统组成,每个子系统又由若干模块(程序)组成。所以,可把调试工作分为模块(程序)调试、分调(子系统调试)和总调(系统调试)三个层次,调试过程依次是模块调试、分调、总调,如图7-2-6所示。

图7-2-6 系统调试的步骤

下面来具体讨论管理信息系统调试的三个步骤。

(1)模块调试

模块(程序)调试的目的是保证每个模块本身能正常运行,在该步调试中发现的问题大都是程序设计或详细设计中的错误。对于模块调试,一般分成人工走通和上机调试两步进行。

人工走通就是打印出源程序,然后参照设计说明书(包括程序框图)的要求把呈现在纸上“走”一遍。程序的错误可分成语法错误和逻辑错误两种情况,一般只要认真检查就可以发现绝大部分的语法错误和部分逻辑错误。而用计算机进行交互调试时,每发现一个错误后要先改正错误才能继续调试,速度要明显降低。所以,决不要一开始就将源程序键入计算机而忙于立即执行,而应先在纸上走通。

程序的检查最好请审查小组或其他开发者。因为程序编制者在审查时往往会犯编程时同样的错误,而查不出某些问题。但这只是理想的情况,由于人力、财力所限,目前的调试基本上还是由编程者本人进行。按各层次人员的分工,模块调试应由操作员或程序员来进行。

当人工走通以后,就可以上机调试了。总的来看,语法错误比较容易发现和修改(因为高级语言都具备语法检查功能,但是检查的全面性不尽相同。为了有效地发现并改正逻辑错误,一方面,可认真设计调试用例,另一方面,要充分利用所用高级语言提供的调试机制或软件工具。

(2)分调

分调也称子系统调试,就是把经过调试的模块放在一起形成一个子系统来调试。主要是调试各模块之间的协调和通信,即重点调试子系统内各模块的接口。例如,数据穿过接口时可能丢失;一个模块对另一个模块可能存在因疏忽而造成的有害影响;把若干子功能结合起来可能不产生预期的主功能等等。

如何将若干个模块连接成一个可运行的子系统,通常有两种方法。一种方法是先分别调试每个模块,再把所有模块按设计要求连成一起进行调试,这种方法称为“非渐增式”调试。另一种方法是把下一个要调试的模块同已经调试好的那些模块结合起来进行调试,调试完成后再把下一个应该调试的模块结合进来调试,这种方式称为“渐增式”,这种方式实际上同时完成了模块调试和子系统调试。

图7-2-7 子系统调试示例

下面以图7-2-7为例讨论这两种方式的差别。

<1>非渐增式

如图7-2-7所示,非渐增式是先分别调试六个模块A、B、C、D、E、F,然后将它们连接到一起再进行调试。若采用这种方式则在调试某个模块X时,需要临时为它设计一个驱动模块和若干个桩模块,如图7-2-8所示。驱动模块的作用是模拟X的调用模块,桩模块的作用则是模拟X的下层模块。例如,调试图7-2-7中的模块B时,要为它设计一个驱动模块,其作用是将调试数据传送给模块B并接收和显示B产生的结果,同时,因B要调用模块E,所以还需设计一个桩模块,用来接受B的控制并模拟E的功能。这儿的临时模块(驱动模块和桩模块)可以设计的非常简单,只要满足调试要求即可。

图7-2-8 驱动模块和桩模块

<2>渐增式

对渐增式来说,又可分为“由顶向下”、“由底向上”等多种方式进行调试。若对图7-2-7采用“由底向上”的渐增式方式,则是先顺序地或并行地(例如由三人完成)调试模块E、C、F,此时只需为每个模块临时准备驱动模块,但不需要桩模块,然后为模块B准备一个驱动模块,将模块B与模块E连接起来调试,再为模块D准备一个驱动模块将D与F连接起来调试,最后把模块A与其它各模块连接并调试。

<3> 两种调试方法的比较

对这两种调试方法进行比较可以看到:

①非渐增式需要更多的人工(如准备较多的控制模块和桩模块),而渐增式则可利用已经调试过的模块(如采用“由底向上”时可不需桩模块)。

②渐增式可以较早地发现模块界面之间的错误,非渐增式则要到最后将所有模块相连时才能发现这类错误。

③渐增式有利于排错。如果界面有错,它通常与最新加上去的那个模块有关,错误比较容易定位,非渐增式则不然。

④渐增式比较彻底。它以前面调试过的模块作为驱动模块或桩模块,所以这些模块将得到进一步的检查。

⑤渐增式需要较多的机器时间。例如在图7-2-7中若采用“由底向上”的渐增式,则在调试模块A时,模块B、C、D、E、F也要执行。若用非渐增式,在调试模块A时只要执行用来模拟B、C、D的桩模块即可。当然,编写这些桩模块业需花费一定机器时间,所以可抵消一部分机器时间。

⑥使用非渐增式可以并行(同时)同时所有模块,能充分利用人力,这对开发系统是很有意义的。

综上所述,可以认为渐增式调试方法较非渐增式要优越。尤其对管理信息系统软件来说“由底向上”的渐增式方法是一种较为适合的调试方法。

当然,在调试一个实际系统时,并没有必要机械照搬上述某一些方法。例如,当把一个已经充分调试过的模块结合进来时,可以着重调试模块之间的接口,当一个没有充分调试过的模块结合进来时,则需要利用已调试过的模块充分调试它。

(3)总调

经过分调,已经把一个模块装成若干子系统并经充分调试。接着的任务是总调,也称为系统调试,它是经过调试的子系统装配成一个完整的系统来调试,用以发现系统设计和程序设计中的错误,验证系统的功能是否达到设计说明书的要求。

刚开始总调时,不必按完全真实情况下的数据量进行,可采用一些精心设计的数据量较少的调试用例,这样不仅可以使处理工作量大为减少,而且更容易发现错误和确定错误所在范围。

什么样的系统是有效的呢?一般说来,当系统的功能和性能如同用户所合理地期待的那样,则系统是有效的。因为系统分析阶段产生的系统说明书,描述了用户的这种合理期望,所以它是系统有效性的标准。

(4)系统调试

总调完成后下一步就可将原始系统手工作业方式得出的结果正确的数据作为新系统的输入数据进行“真实”运行,这时除了将结果与手工作业进行校核以外,还应考察系统的有效性、可靠性和效率。为此,最好请用户一起参加系统调试工作。系统调试的关键是“真实”和全面。进行系统调试应该注重以下几点:

<1>调试用例应该是由实际意义的数据组成的。可以请用户参与调试用例的设计。

<2>某些已经调试过的纯粹技术的特点可以不需再次执行。

<3>对用户特别感兴趣的功能或性能,可以增加一些调试。

<4>应该设计并执行一些与用户使用步骤有关的调试。

在总调和系统调试之前必须有充分准备,尽量使用户能够积极主动地参与,特别是为了使用户能有效地使用该系统,通常在总调之前由开发部门对用户进行培训。

在总调阶段发现的问题往往和系统分析阶段的差错有关,涉及面较广且解决起来也较困难,这时需要和用户充分协商解决。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织