目录
9 软件测试
9-1 软件测试基础
第1关:软件测试的目标和准则
任务描述
本关任务:根据所学知识,完成右侧的选择题和判断题。
相关知识
为了完成本关任务,你需要掌握:软件测试的目标和准则。
软件测试的目标
G.Myers给出了关于测试的一些规则,这些规则也可以看作是测试的目标或定义。
(1) 测试是为了发现程序中的错误而执行程序的过程;
(2) 好的测试方案是极可能发现迄今为止尚未发现的错误的测试方案;
(3) 成功的测试是发现了至今为止尚未发现的错误的测试。
测试决不能证明程序是正确的。
即使经过了最严格的测试之后,仍然可能还有没被发现的错误潜藏在程序中。
测试只能查找出程序中的错误,不能证明程序中没有错误。
测试目标决定了测试方案的设计:
(1)如果为了表明程序是正确的而进行测试,就会设计一些不易暴露错误的测试方案;
(2)如果测试是为了发现程序中的错误,就会力求设计出最能暴露错误的测试方案。
软件测试的准则
为了能设计出有效的测试方案,软件工程师必须深入理解并正确运用指导软件测试的基本准则。
(1)所有测试都应该能追溯到用户需求。
从用户的角度看,最严重的错误是导致程序不能满足用户需求的那些错误。
(2)应该远在测试开始之前就制定出测试计划。
因为一旦完成了需求模型就可以着手制定测试计划,建立设计模型后就可立即开始设计详细的测试方案。所以在编码之前就可对所有测试工作进行计划和设计。
(3)把Pareto原理应用到软件测试中。
测试发现的80%错误很可能是由程序中20%的模块造成的。
(4)应该从“小规模”测试开始,逐步进行“大规模”测试。
单个程序模块 ➔ 集成的模块簇 ➔ 整个系统
(5)穷举测试是不可能的。
穷举测试: 把程序所有可能的执行路径都检查一遍。
即使是一个中等规模的程序,其执行路径的排列数也十分庞大,由于受时间、人力和资源的限制,在测试过程中不可能执行每个可能的路径。因此,测试只能证明程序中有错误,不能证明程序中没有错误。
但是,精心地设计测试方案,有可能充分覆盖程序逻辑并使程序达到所要求的可靠性。
(6)为达到测试最佳效果,应由独立的第三方从事测试工作
最佳效果:有最大可能性发现错误的测试。
开发软件的软件工程师主要承担模块测试工作。
第2关:软件测试的方法和步骤
任务描述
本关任务:根据所学知识,完成右侧的选择题和判断题。
相关知识
为了完成本关任务,你需要掌握:软件测试的方法和步骤。
软件测试的方法
测试任何产品都有两种方法:
(1)黑盒测试(Black-box Testing)
已经知道了产品应该具有的功能,通过测试检验是否每个功能都能正常使用。
黑盒测试又称功能测试,把程序看作一个黑盒子,完全不考虑程序的内部结构和处理过程。
在程序接口进行的测试,它只检查程序功能是否能按照规格说明书的规定正常使用,程序是否能适当地接收输入数据并产生正确的输出信息,程序运行过程中能否保持外部信息的完整性。
(2)白盒测试(White-box Testing)
知道产品的内部工作过程,通过测试来检验产品内部动作是否按照规格说明书的规定正常进行。
白盒测试又称为结构测试,测试者完全知道程序的结构和处理算法。这种方法按照程序内部的逻辑测试程序,检测程序中的主要执行通路是否都能按预定要求正确工作。
黑盒测试是从用户观点的测试,白盒测试是从开发人员观点的测试。
软件测试的步骤
大型软件系统的测试过程包含下述几个步骤:
(1)单元测试(Unit Testing)
在设计得好的软件系统中,每个模块完成一个清晰定义的子功能,而且这个子功能和同级其他模块的功能之间没有相互依赖关系。因此,有可能把每个模块作为一个单独的实体来测试,而且通常比较容易设计检验模块正确性的测试方案。
目的:保证每个模块作为一个单元能正确运行。
所发现的错误:往往是编码和详细设计的错误。
(2)子系统测试(Subsystem Testing)
把经过单元测试的模块放在一起形成一个子系统来测试。
测试过程中的主要问题:模块相互间的协调和通信。
测试重点:模块的接口。
(3)系统测试(System Testing)
把经过测试的子系统装配成一个完整的系统来测试。
测试目的:
a.发现设计和编码的错误。
b.验证系统确实能提供需求说明书中指定的功能。
c.系统的动态特性符合预定要求。
子系统测试、系统测试,通常称为集成测试。
(4)验收测试(Acceptance Testing)
把软件系统作为单一的实体进行测试。
测试内容与系统测试基本类似,但它是在用户积极参与下进行的,而且可能主要使用实际数据(系统将来要处理的信息)进行测试,发现的往往是系统需求说明书中的错误。
测试目的:验证系统确实能够满足用户的需要。
(5)平行运行(Parallel Operation)
同时运行新开发出来的系统和将被它取代的旧系统,以便比较新旧两个系统的处理结果。
这样做的具体目的有如下几点:
a.可以在准生产环境中运行新系统而又不冒风险;
b.用户能有一段熟悉新系统的时间;
c.可以验证用户指南和使用手册之类的文档;
d.能够以准生产模式对新系统进行全负荷测试,可以用测试结果验证性能指标。
9-2 黑盒测试
第1关:等价类划分法
任务描述
本关任务:使用等价类划分法进行黑盒测试。
相关知识
为了完成本关任务,你需要掌握等价类划分法。
等价类划分法的概念
等价类划分法(Equivalence partitioning):根据需求对输入范围进行细分,把输入分成几个区域,然后在每一区域里选取有代表性的测试用例开展测试的方法。
等价类是指某个输入域的子集合,各个输入数据对于揭露程序中的错误都是等效的。测试某等价类的代表值就等价于对这一类其他值的测试。
有效等价类:是指符合《需求规格说明书》,合理的输入数据集合。
无效等价类:是指对于程序的规格说明书来说,是不合理的,无意义的输入数据集合。
等价类划分法的步骤
1.在确立等价类之后,可列出所有划分的等价类表。
2.为每一个等价类规定一个唯一的编号。
3.设计一个新的测试用例,使其尽可能多地覆盖尚未被覆盖地有效等价类,重复这一步,直到所有的有效等价类都被覆盖为止。
4.设计一个新的测试用例,使其仅覆盖一个尚未被覆盖的无效等价类,重复这一步,直到所有的无效等价类都被覆盖为止。
等价类划分法的特点
1.完全不考虑程序的内部结构,只依据系统需求分析说明书来设计测试用例,是黑盒测试方法。
2.等价类划分的方法是把程序的输入划分成若干部分,然后从每个部分中选取少数代表性数据当作测试用例。每一类的代表性数据在测试中的作用等价于这一类中的其他值。
等价类划分的原则
1.如果输入条件规定了取值范围,则可以确定一个有效等价类(输入值在此范围内)和两个无效等价类(输入值小于最小值及大于最大值)。
2.如果输入条件规定了值的个数,则可以确定一个有效等价类(输入值的个数等于规定的个数)和两个无效等价类(输入值的个数小于规定的个数和大于规定的个数)。
3.如果输入条件规定了输入值的集合,而且程序对不同的输入值做不同的处理,那么每个允许的值都确定为一个有效等价类,另外还有一个无效等价类(任意一个不允许的值)。
例如,规定输入的考试成绩为A、B、C、D、E,则可确定5个有效等价类(成绩=A、成绩=B、成绩=C、成绩=D、成绩=E)和一个无效等价类(成绩=A、B、C、D、E之外的值)。
4.如果输入条件规定了输入值必须遵循的规则,那么可确定一个有效等价类(符合此规则)和若干个无效等价类(从各个不同的角度违反此规则)。
例如,在某语言中对变量标识符规定为“以字母开头”。那么有效等价类是“以字母开头”,而无效等价类有“以数字开头”,“以标点符号开头”,”以下划线开头“等等。
5.如果输入条件是一个布尔量,则可以确定一个有效等价类和一个无效等价类。
例如:程序输入条件为BOOL x=true,则有效等价类为x=true,无效等价类为x=false。
6.如果某一个等价类中的各元素在程序处理中有区别,那么就需要把此等价类划分为更小的等价类。
例如:程序规定对实数做取整操作,取整后如果能被3整除,则做相应处理。那么,我们对实数这个等价类就需要再细分为更小的等价类。
第2关:边界值分析法
任务描述
本关任务:使用边界值分析法进行黑盒测试。
相关知识
为了完成本关任务,你需要掌握:1.如何获取数组的长度,2.如何遍历数组。
边界值分析法的概念
边界值分析法(Boundary Value Analysis)就是,针对于输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是对等价类划分法的一种补充。
人们从测试工作经验得知,大量的错误是发生在输入或输出范围的边界上,而不是在输入范围的内部。因此针对各种边界情况设计测试用例,可以查出更多的错误。
在这种情况下,不是选择等价类的任意元素,而是选择相对于输入等价类和输出等价类而言,稍高于其边界值及稍低于其边界值的一些特定情况。
边界值点的定义:
上点:边界上的点,闭内开外(闭指域的边界是封闭的,即闭区间;开指域的边界是开放的,即开区间)。
离点:离上点最近的点称为离点。开内闭外。
内点:域范围内的任意一点。
等价类划分法的步骤
1.根据划分的等价类,确定边界情况。
2.选取正好等于,刚刚大于,或刚刚小于边界的值作为测试数据。
等价类划分的原则
1,如果输入条件规定了值的范围,则应取刚达到这个范围的边界的值,以及刚刚超越这个范围边界的值作为测试输入数据。
2.如果输入条件规定了值的个数,则用最大个数,最小个数,比最大个数多1,比最小个数少1的数作为测试数据。
3.根据规格说明的每一个输出条件,使用前面的原则1、原则2。
4.如果程序的规格说明给出的输入域或输出域是有序集合(如有序表,顺序文件等),则应选取集合的第一个元素和最后一个元素作为测试用例。
5.如果程序中使用了一个内部数据结构,则应当选择这个内部数据结构的边界上的值作为测试用例。
6.仔细阅读需求分析说明书,找到一切隐含的边界。
边界值和等价类划分法的关系
等价类划分法:将测试过程中的输入、输出、操作等相似内容分组,从每组中挑选具有代表性的内容作为测试用例,划分为有效等价类和无效等价类;
边界值分析法:确认输入、输出的边界,然后取刚好等于、大于、小于边界的参数作为测试用例测试;
边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充,这种情况下,其测试用例来自等价类的边界。
实践中,由于大量的错误发生在输入、输出值的边界上,所以,对于各种边界值进行测试用例的设计,可以查出更多的错误。
三点分析法
结合等价类划分的具体情况,针对边界值的选择就包括开区间、闭区间以及半开半闭区间。
(1)闭区间(Closed Interval):闭区间中的情况,上点为可以取值的点,在上点之间任取一点就是内点。而紧邻上点范围之外的第一对点被称为离点。
(2)半开半闭区间(Semi Open and Semi Closed Interval):半开半闭区间中,上点与内点的定义不变。离点是开区间一侧上点内部范围内紧邻的点,而在闭区间一侧是上点外部范围内紧邻的点。
(3)开区间(Open Interval):开区间中,上点与内点的定义仍然不变。而离点就是上点内部范围内紧邻的一对点。
总结:上点就是区间的端点值,而内点就是上点之间任意一点。对于离点,要分具体情况,如果开区间的离点,就是开区间中上点内侧紧邻的点;如果是闭区间的离点,就是闭区间中上点外侧紧邻的点。
常用的边界值
对16-bit的整数而言,32767和-32768是它的边界。
屏幕上光标在最左上,最右下的位置。
数据库表(或报表)的第一行和最后一行。
数组元素的第一个和最后一个。
循环的第0次,第1次,倒数第2次和最后一次。
字符串中的第一个字符和最后一个字符。
U盘(内存,物理存储)的空和满。
操作反应(比如:登录操作,触屏操作)的最快和最慢。
9-3 白盒测试
第1关:逻辑覆盖-语句覆盖
任务描述
本关任务:以闯关的模式,让学生在自己设计测试用例的过程中一步一步掌握逻辑覆盖的各个等级。
相关知识
什么是逻辑覆盖
逻辑覆盖(Logic Cover)是以程序内部的逻辑结构为基础的设计测试用例的技术,是一种重要的白盒测试技术。它是一系列测试过程的总称,这组测试过程逐渐进行越来越完整的通路测试。
根据覆盖源程序语句的详尽程度,大致有以下一些不同的覆盖标准:语句覆盖、判定覆盖、条件覆盖、判定/条件覆盖、条件组合覆盖、路径覆盖。在接下来的关卡中,将分别讲述如何用上述覆盖标准设计测试用例,并让同学们动手实践。
语句覆盖
定义
为了暴露程序中的错误,程序中的每个语句至少应该执行一次。语句覆盖(Statement Coverage)的含义是,设计足够多的测试用例,使被测程序中的每个语句至少执行一次。
特点
1、程序中每一个语句至少执行一次
2、对程序执行逻辑的覆盖率低,属于最弱的覆盖方式
3、无需测试程序的分支情况
4、无需测试程序分支判断的输入值以及输入值的组合
5、无需测试程序执行的不同路径
图解
如上图,若测试用例的设计满足A ^ B = T,则满足了语句覆盖。
实例
假设需要测试这样的代码:
画出该程序的流程图:
为了使每个语句都执行一次,程序的执行路径应该是acbed,为此只需要输入下面的测试用例:(A = 2,B = 0,X为任意实数)便可以满足语句覆盖。
我们发现,语句覆盖对程序的逻辑覆盖很少,在上面这个例子中,两个判定都只测试了判定为真的情况,如果判定为假时有错误,显然不能发现。此外,语句覆盖只关心判定表达式的值,而没有分别测试判定表达式中每个条件取不同值时的情况。
再看一个实例
假设需要测试这样的代码:
假如测试人员编写了如下测试用例:
TeseCase: a = 10, b = 5,result=2
很明显,这样的测试用例是满足语句覆盖的,因为foo函数中的语句都至少被执行了一次。但是这样的测试用例找不出b不能为0的bug。
从这个例子不难看出,语句覆盖难以发现程序中逻辑运算的错误。
总结
综上所述,语句覆盖是很弱的逻辑覆盖标准,难以发现程序中的逻辑错误,为了更充分地测试程序,需采用更强的逻辑覆盖标准。
第2关:逻辑覆盖-判定覆盖
任务描述
本关任务:以闯关的模式,让学生在自己设计测试用例的过程中一步一步掌握逻辑覆盖的各个等级。
相关知识
判定覆盖
定义
判定覆盖(Decision Coverage)又叫分支覆盖,它的含义是,不仅程序中每个语句必须执行至少一次,而且程序中每个判定的每种可能结果都应该至少执行一次。也就是说,设计足够多的测试用例,使得程序中的每一个判断至少获得一次“真”和一次“假”,即使程序流程图中的每一个真假分支至少被执行一次。
特点
1.满足判定覆盖的测试用例一定满足语句覆盖
2.对整个判定的最终取值(真或假)进行度量,但判定内部每一个子表达式的取值未被考虑。
3.判定覆盖比语句覆盖要多几乎一倍的测试路径,当然也就具有比语句覆盖更强的测试能力。同样判定覆盖也具有和语句覆盖一样的简单性,无须细分判定中的每个条件就可以得到测试用例。
4.往往大部分的判定语句是由多个逻辑条件组合而成,若仅仅判断其整个最终结果,而忽略每个条件的取值情况,必然会遗漏部分测试路径。
图解
如上图,若测试用例的设计既覆盖到A ^ B = T这种情况,又覆盖到A ^ B = F这种情况,则这组测试用例的设计满足判定覆盖。
实例
假设需要测试这样的代码:
画出该程序的流程图:
由流程图可知,能够分别覆盖路径acd和abe的两组测试用例,或者能够分别覆盖路径ace和abd的两组测试用例,都满足判定覆盖标准。
例如,用下面两组测试用例就可以满足判定覆盖。
1.A=3,B=0,X=1,result=1/3(覆盖acd)
2.A=2,B=1,X=1,result=4(覆盖abe)
总结
判定覆盖比语句覆盖强,但是对程序逻辑的覆盖程度仍不高,例如,上面的测试用例只覆盖了程序全部路径的一半。
第3关:逻辑覆盖-条件覆盖
第4关:逻辑覆盖-判定/条件覆盖
第5关:逻辑覆盖-条件组合覆盖
第6关:逻辑覆盖-路径覆盖
任务描述
本关任务:以闯关的模式,让学生在自己设计测试用例的过程中一步一步掌握逻辑覆盖的各个等级。
相关知识
路径覆盖
定义
路径覆盖(Path Coverage)的含义是,选取足够多的测试用例,覆盖程序中所有可能的执行路径。(如果程序图中有环,则要求每个环至少经过一次)。
特点
1.这种覆盖方法可以对程序进行彻底的测试用例覆盖,比前面讲的五种方法的覆盖度都要高。
2.路径覆盖需要对所有可能的路径进行测试(包括循环、条件组合、分支选择等),那么需要设计大量、复杂的测试用例,使得工作量呈指数级增长。路径覆盖虽然是一种比较强的覆盖,但未必考虑判断语句中条件表达式结果的组合,并不能代替条件覆盖和条件组合覆盖。
3.路径覆盖率的公式:
路径覆盖率 = 被执行到的路径数 / 程序中总的路径数。
4.在有些情况下,一些执行路径是不可能被执行的,如:
if(!A) B++;
if(!A) D--;
这两个语句实际只包括了2条执行路径,即A为真或假时候对B和D的处理,真或假不可能都存在,而路径覆盖测试则认为是包含了真与假的4条执行路径。这样不仅降低了测试效率,而且大量的测试结果的累积,也为排错带来麻烦。
图解
若要对如上的程序流程图使用路径覆盖法设计测试用例,则测试用例需覆盖程序中所有可能的执行路径,共有4条:路径abd,路径abe,路径acf,路径acg。
实例
假设需要测试这样的代码:
画出该程序的流程图:
由流程图可知,该例子中共有4条执行路径,分别为路径
总结
路径覆盖是白盒测试逻辑覆盖的6种覆盖标准中最强的,测试了程序中所有可能的执行路径。但是,路径覆盖需要设计大量、复杂的测试用例,使得工作量呈指数级增长。路径覆盖虽然是一种比较强的覆盖,但未必考虑判断语句中条件表达式结果的组合,并不能代替条件覆盖和条件组合覆盖。
第7关:基本路径测试
9-4 单元测试
第1关:使用Junit进行单元测试
任务描述
本关任务:使用Junit对给定的计算器类(Calculator.java)进行单元测试。
相关知识
单元测试的定义
单元测试(Unit testing)是对最小的软件设计单元(模块或源程序单元)的验证工作。
在设计得好的软件系统中,每个模块完成一个清晰定义的子功能,而且这个子功能和同级其他模块的功能之间没有相互依赖关系。因此,有可能把每个模块作为一个单独的实体来测试,而且通常比较容易设计检验模块正确性的测试方案。
目的:保证每个模块作为一个单元能正确运行。
所发现的错误:往往是编码和详细设计的错误。
单元测试的测试重点
单元测试集中检测软件设计的最小单元——模块。
在单元测试期间着重从下述5个方面对模块进行测试。
1.模块接口
首先应该对通过模块接口的数据流进行测试,如果数据不能正确地进出,所有其他测试都是不切实际的。
在对模块接口进行测试时主要检查下述几个方面:参数的数目、次序、属性或单位系统与变元是否一致;是否修改了只作输入用的变元;全局变量的定义和用法在各个模块中是否一致。
2.局部数据结构
对于模块来说,局部数据结构是常见的错误来源。应该仔细设计测试方案,以便发现局部数据说明、初始化、默认值等方面的错误。
3.重要的执行通路
由于通常不可能进行穷尽测试,因此,在单元测试期间选择最有代表性、最可能发现错误的执行通路进行测试就是十分关键的。应该设计测试方案用来发现由于错误的计算、不正确的比较或不适当的控制流而造成的错误。
4.出错处理通路
好的设计应该能预见出现错误的条件,并且设置适当的处理错误的通路,以便在真的出现错误时执行相应的出错处理通路或干净地结束处理。不仅应该在程序中包含出错处理通路,而且应该认真测试这种通路。当评价出错处理通路时,应该着重测试下述一些可能发生的错误:
(1) 对错误的描述是难以理解的;
(2) 记下的错误与实际遇到的错误不同;
(3) 在对错误进行处理之前,错误条件已经引起系统干预;
(4) 对错误的处理不正确;
(5) 描述错误的信息不足以帮助确定造成错误的位置。
5. 边界条件
边界测试是单元测试中最后的也可能是最重要的任务。
软件常常在它的边界上失效。
例如,处理n元数组的第n个元素时,或做到i次循环中的第i次重复时,往往会发生错误。使用刚好小于、刚好等于和刚好大于最大值或最小值的数据结构、控制量和数据值的测试方案,非常可能发现软件中的错误。
单元测试的局限性
单元测试不能捕获程序中的每一个错误。根据定义,单元测试只测试单元自身的功能。
因此它不捕获集成错误、性能问题或其它任何系统范围的问题。
另外,要预料现实中被测程序可能接受到的输入的所有特殊情况是一项不易之事。
对于任何非平凡的软件块要测试所用的输入组合是不现实的。
Junit是什么
JUnit 是一个 Java 编程语言的单元测试框架。JUnit 在测试驱动的开发方面有很重要的发展,是起源于 JUnit 的一个统称为 xUnit 的单元测试框架之一。
几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试。
Junit好处及作用
好处:
1.可以书写一系列的测试方法,对项目所有的接口或者方法进行单元测试。
2.启动后,自动化测试,并判断执行结果, 不需要人为的干预。
3.只需要查看最后结果,就知道整个项目的方法接口是否通畅。
4.每个单元测试用例相对独立,由Junit 启动,自动调用。不需要添加额外的调用语句。
5.添加,删除,屏蔽测试方法,不影响其他的测试方法。 开源框架都对JUnit 有相应的支持。
作用:
通常我们写完代码想要测试这段代码的正确性,那么必须新建一个类,然后创建一个 main() 方法,之后再编写测试代码。如果需要测试的代码很多呢?那么要么就会建很多main() 方法来测试,要么将其全部写在一个main()方法里面。这也会大大的增加测试的复杂度,降低程序员的测试积极性。而 Junit 能很好的解决这个问题,使用Junit后,可以非常简单地组织测试代码,并随时运行它们,Junit就会给出成功的测试和失败的测试,还可以生成测试报告。
Junit使用规范
一是单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;
二是每个单元测试应当互相独立,不依赖运行的顺序;
三是测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0,null,空字符串""等情况。
Junit断言
使用JUnit进行单元测试,我们可以使用断言(Assertion)来测试期望结果,即使用断言方法判断期望值和实际值差异,返回Boolean值。这样一来,可以方便地组织和运行测试,并方便地查看测试结果。
Junit所有的断言都包含在 Assert 类中。
Assert 类中的一些有用的方法列式如下:
void assertEquals(boolean expected, boolean actual):检查两个变量或者等式是否平衡
void assertTrue(boolean expected, boolean actual):检查条件为真
void assertFalse(boolean condition):检查条件为假
void assertNotNull(Object object):检查对象不为空
void assertNull(Object object):检查对象为空
void assertSame(boolean condition):assertSame() 方法检查两个相关对象是否指向同一个对象
void assertNotSame(boolean condition):assertNotSame() 方法检查两个相关对象是否不指向同一个对象
void assertArrayEquals(expectedArray, resultArray):assertArrayEquals() 方法检查两个数组是否相等
Junit注解
Java注解(Annotation)的使用方法是”@ + 注解名” 。借助注解,我们可以在编程中通过简单的注解来实现一些功能。
JUnit要求对核心测试方法加上@Test注解,它会把带有@Test的方法识别为测试方法。除此之外,还有以下几个比较常用的注解。
@Before:有些测试在运行前需要创造几个相似的对象。在 public void 方法加该注释是因为该方法需要在 test 方法前运行。
@After:如果你将外部资源在 Before 方法中分配,那么你需要在测试运行后释放他们。在 public void 方法加该注释是因为该方法需要在 test 方法后运行。
@BeforeClass:在 public void 方法加该注释是因为该方法需要在类中所有方法前运行。
@AfterClass:它将会使方法在所有测试结束后执行。这个可以用来进行清理活动。
@Ignore:这个注释是用来忽略有关不需要执行的测试的。
Junit加注解执行过程:
beforeClass(): 方法首先执行,并且只执行一次。
afterClass():方法最后执行,并且只执行一次。
before():方法针对每一个测试用例执行,但是是在执行测试用例之前。
after():方法针对每一个测试用例执行,但是是在执行测试用例之后。
在 before() 方法和 after() 方法之间,执行每一个测试用例。
如何编写Junit测试
首先,我们将介绍一个测试类Calculator.java。
//Calculator.java
public class Calculator {
public int add(int a,int b){
return a + b;
}
}
如上方代码所示,Calculaor类有一个公共的方法add(), 它接收输入两个整数,将它们相加并返回结果。在这里,我们将测试这个方法。具体如何来做呢?
首先,创建一个测试类CalculatorTest.java。引入junit相关包,借助junit注解和断言来实现对add方法的测试,代码如下:
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CalculatorTest {
//引入Calculator对象
Calculator calcu = new Calculator();
/*
请在下面的Begin/End内写一个测试函数,
来验证Calculator中的add函数编写是否正确
*/
/***********************Begin**************************/
@Test
public void testAdd(){
int res = calcu.add(3,4);
int actualres = 7;
assertEquals(res,actualres);
}
/************************End***************************/
}
来看一下上方的代码。首先,我们可以看到,有一个@Test的注解在 testAdd()方法的上方。 这个注解指示该方法所附着的代码可以做为一个测试用例。因此,testAdd()方法可用于测试公开方法 add()。我们再观察一个方法 assertEquals(res, actualres)。 该方法的作用是判断预期输出与实际输出是否相等。
编程要求
本关的编程任务是补全CalculatorTest类中的代码,对Calculator类进行单元测试。Calculator类是一个简易的计算器,实现了基础的加减乘除功能。同学们需要仿照上方的代码,在CalculatorTest类中补全测试函数testAdd, testSub, testMultiply, testDivide, testDivideByZero分别对计算器的加、减、乘、除功能进行测试。
注:每个测试方法各设计1个测试样例,除操作需考虑除数为0的情况。
本关涉及的Calculator类代码如下所示:
//Calculator.java
public class Calculator {
//加
public int add(int a,int b){
return a + b;
}
//减
public int sub(int a,int b){
return a - b;
}
//乘
public int multiply(int a,int b){
return a * b;
}
//除
public int divide(int a,int b) throws Exception {
if(b == 0)
{
throw new Exception("除数不能为0");
}
return a / b;
}
}
测试说明
同学们需要仿照上一小节《如何编写Junit测试》中的代码,对右侧代码中的每一个测试函数testAdd, testSub, testMultiply, testDivide, testDivideByZero分别设计一个测试样例,补全测试函数。其中,除数为0的情况在测试函数testDivdeByZero中体现。
本关卡的测试文件已进行封装,学生不可见,用于验证学生的Junit测试代码是否编写正确。
具体测试过程如下:
1.平台自动编译生成TestRunner.exe;
2.平台运行TestRunner.exe;
3.获取TestRunner.exe输出,并将其输出与预期输出对比:如果一致则测试通过,否则测试失败。
第2关:改进——Junit参数化测试
任务描述
本关任务:使用Junit参数化测试,对简易计算器类(Calculator.java)进行单元测试。
相关知识
为了完成本关任务,你需要掌握:
1.为什么需要参数化测试
2.Junit参数化测试流程
3.如何编写参数化测试代码
为什么需要参数化测试
经过上一小节的学习,我们已经学会使用Junit注解和断言来编写简单的单元测试代码。我们在每一个测试方法中都各设计了一个测试用例。但是,假如我们想对每个测试方法设计多个测试用例,代码就会变得冗余,比较麻烦。那么,有没有什么好办法解决这个问题呢?
Junit参数化测试(Parameterized tests)很好的解决了这个问题。如果测试代码大同小异,代码结构都是相同的,不同的只是测试的数据和预期值,那么Junit的参数化测试可以派上用场了——它允许使用不同的参数多次运行同一个测试。
Junit参数化测试流程
我们遵循以下5个步骤来创建参数化测试。
1.用 @RunWith(Parameterized.class) 来注释 test 类。
2.创建一个由 @Parameters 注释的公共的静态方法,它返回一个对象的集合(数组)来作为测试数据集合。
3.创建一个公共的构造函数,它接受和一行测试数据相等同的东西。
4.为每一列测试数据创建一个实例变量。
5.用实例变量作为测试数据的来源来创建你的测试用例。
在JUnit中,可以使用@RunWith 和@Parameters这两个注解来为单元测试传递参数。
@RunWith注解:当类被@RunWith注解修饰,或者类继承了一个被该注解修饰的类,JUnit将会使用这个注解所指明的运行器(runner)来运行测试。
@Parameters注解:然后在该类提供数据的方法上加上一个@Parameters注解,这个方法必须是静态static的,不能带参数,并且返回一个集合Collection。
如何编写参数化测试代码
我们通过一个具体示例来演示如何编写参数化测试代码。
我们继续使用上一关卡中计算器类中的add方法作为案例教学。
代码如下:
//Calculator.java
public class Calculator {
//加
public int add(int a,int b){
return a + b;
}
//减
public int sub(int a,int b){
return a - b;
}
//乘
public int multiply(int a,int b){
return a * b;
}
//除
public int divide(int a,int b) throws Exception {
if(b == 0)
{
throw new Exception("除数不能为0");
}
return a / b;
}
}
为其创建一个参数化测试类CalculatorTest2.java。
代码如下:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
//参数化测试类需用@RunWith注解修饰
@RunWith(Parameterized.class)
public class CalculatorTest2 {
private int a;
private int b;
private int expect;
@Parameterized.Parameters
//prepareData方法会将多个测试用例作为一个Collection返回
//Collection中的每个元素都会调用一遍CalculatorTest2类的构造方法
//每调用一次构造方法,都会产生一个类对象,每个类对象都会去执行testAdd()方法
//这样一来,便实现了使用不同的测试用例运行同一个测试方法的功能
public static Collection prepareData(){
Object [][] object = {{1,2,3},{-1,0,-1},{0,0,0},{-1,-2,-3}};
return Arrays.asList(object);
}
public CalculatorTest2(int a,int b,int expect){
this.a = a;
this.b = b;
this.expect = expect;
}
@Test
public void testAdd(){
Calculator calcu = new Calculator();
int res = calcu.add(a,b);
assertEquals(expect,res);
}
}
编程要求
仿照上面的方法,使用Junit参数化测试对计算器类中的multiply方法进行测试,依据要求在题目中的相应位置补全代码。
9-5 集成测试和确认测试
第1关:集成测试
任务描述
本关任务:根据所学知识,完成右侧的选择题和判断题。
相关知识
为了完成本关任务,你需要掌握集成测试的相关知识。
集成测试的概念
集成测试(Integration Testing)是把单独的软件模块结合在一起作为整体接受测试。实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常的工作。一些局部反映不出来的问题,在全局上很可能暴露出来。
非渐增式测试和渐增式测试
由模块组装成程序时有两种方法。
非渐增式测试(Non Incremental Testing)方法:先分别测试每个模块,再把所有模块按设计要求放在一起结合成所要的程序;
渐增式测试(Incremental Testing)方法:把下一个要测试的模块同已经测试好的那些模块结合起来进行测试,测试完以后再把下一个应该测试的模块结合进来测试。每次增加一个模块的方法,这种方法实际上同时完成单元测试和集成测试。
非渐增式测试一下子把所有模块放在一起,并把庞大的程序作为一个整体来测试,测试者面对的情况十分复杂。测试时会遇到许许多多的错误,改正错误更是极端困难,因为在庞大的程序中想要诊断定位一个错误是非常困难的。而且一旦改正一个错误之后,马上又会遇到新的错误,这个过程将继续下去,看起来好像永远也没有尽头。
渐增式测试与“一步到位”的非渐增式测试相反,它把程序划分成小段来构造和测试,在这个过程中比较容易定位和改正错误;对接口可以进行更彻底的测试;可以使用系统化的测试方法。
因此,目前在进行集成测试时普遍采用渐增式测试方法。
自顶向下集成和自底向上集成
(1)自顶向下集成(Top-down Integration)
自顶向下集成方法是一个日益为人们广泛采用的测试和组装软件的途径。从主控制模块开始,沿着程序的控制层次向下移动,逐渐把各个模块结合起来。在把附属于(及最终附属于)主控制模块的那些模块组装到程序结构中去时,或者使用深度优先的策略,或者使用宽度优先的策略。
深度优先的结合方法先组装在软件结构的一条主控制通路上的所有模块。选择一条主控制通路取决于应用的特点,并且有很大任意性。而宽度优先的结合方法是沿软件结构水平地移动,把处于同一个控制层次上的所有模块组装起来。
把模块结合进软件结构的具体过程由下述4个步骤完成:
第一步,对主控制模块进行测试,测试时用存根程序代替所有直接附属于主控制模块的模块;
第二步,根据选定的结合策略(深度优先或宽度优先),每次用一个实际模块代换一个存根程序(新结合进来的模块往往又需要新的存根程序);
第三步,在结合进一个模块的同时进行测试; 第四步,为了保证加入模块没有引进新的错误,可能需要进行回归测试(即,全部或部分地重复以前做过的测试)。从第二步开始不断地重复进行上述过程,直到构造起完整的软件结构为止。上图描绘了这个过程。
自顶向下的结合策略能够在测试的早期对主要的控制或关键的抉择进行检验。在一个分解得好的软件结构中,关键的抉择位于层次系统的较上层,因此首先碰到。如果主要控制确实有问题,早期认识到这类问题是很有好处的,可以及早想办法解决。如果选择深度优先的结合方法,可以在早期实现软件的一个完整的功能并且验证这个功能。早期证实软件的一个完整的功能,可以增强开发人员和用户双方的信心。
(2)自底向上集成(Bottom-up Integration)
自底向上测试从“原子”模块(即在软件结构最低层的模块)开始组装和测试。因为是从底部向上结合模块,总能得到所需的下层模块处理功能,所以不需要存根程序。
用下述步骤可以实现自底向上的结合策略:
第一步,把低层模块组合成实现某个特定的软件子功能的族;
第二步,写一个驱动程序(用于测试的控制程序),协调测试数据的输入和输出;
第三步,对由模块组成的子功能族进行测试; 第四步,去掉驱动程序,沿软件结构自下向上移动,把子功能族组合起来形成更大的子功能族。
随着结合向上移动,对测试驱动程序的需要也减少了。事实上,如果软件结构的顶部两层用自顶向下的方法组装,可以明显减少驱动程序的数目,而且族的结合也将大大简化。
回归测试
在集成测试的范畴中,回归测试(Regression Testing)是指重新执行已经做过的测试的某个子集,以保证上述这些变化没有带来非预期的副作用。
更广义地说,任何成功的测试都会发现错误,而且错误必须被改正。每当改正软件错误的时候,软件配置的某些成分(程序、文档或数据)也被修改了。回归测试就是用于保证由于调试或其他原因引起的变化,不会导致非预期的软件行为或额外错误的测试活动。
回归测试可以通过重新执行全部测试用例的一个子集人工地进行,也可以使用自动化的捕获回放工具自动进行。利用捕获回放工具,软件工程师能够捕获测试用例和实际运行结果,然后可以回放(即重新执行测试用例),并且比较软件变化前后所得到的运行结果。
回归测试集(已执行过的测试用例的子集)包括下述3类不同的测试用例:
(1)检测软件全部功能的代表性测试用例;
(2)专门针对可能受修改影响的软件功能的附加测试;
(3)针对被修改过的软件成分的测试。
在集成测试过程中,回归测试用例的数量可能变得非常大。因此,应该把回归测试集设计成只包括可以检测程序每个主要功能中的一类或多类错误的那样一些测试用例。一旦修改了软件之后就重新执行检测程序每个功能的全部测试用例,是低效而且不切实际的。
第2关:确认测试
任务描述
本关任务:根据所学知识,完成右侧的选择题和判断题。
相关知识
为了完成本关任务,你需要掌握确认测试的相关知识。
确认测试的概念
确认测试(Validation Testing)也称为验收测试,它的目标是验证软件的有效性。
确认(validation)和验证(verification)
验证:保证软件正确地实现了某个特定要求的一系列活动
确认:为了保证软件确实满足了用户需求而进行的一系列活动
什么样的软件才是有效的呢?
如果软件的功能和性能如同用户所合理期待的那样,软件就是有效的。
需求分析阶段产生的软件需求规格说明书,准确地描述了用户对软件的合理期望,因此是软件有效性的标准,也是进行确认测试的基础。
确认测试的范围
确认测试必须有用户积极参与,或者以用户为主进行。用户应该参与设计测试方案,使用用户界面输入测试数据并且分析评价测试的输出结果。为了使得用户能够积极主动地参与确认测试,特别是为了使用户能有效地使用这个系统,通常在验收之前由开发单位对用户进行培训。
确认测试通常使用黑盒测试法。应该仔细设计测试计划和测试过程,测试计划包括要进行的测试的种类及进度安排,测试过程规定了用来检测软件是否与需求一致的测试方案。通过测试和调试要保证软件能满足所有功能要求,能达到每个性能要求,文档资料是准确而完整的,此外,还应该保证软件能满足其他预定的要求(例如,安全性、可移植性、兼容性和可维护性等)。
确认测试有下述两种可能的结果:
(1)功能和性能与用户要求一致,软件是可以接受的;
(2)功能和性能与用户要求有差距。
在这个阶段发现的问题往往和需求分析阶段的差错有关,涉及的面通常比较广,因此解决起来也比较困难。为了制定解决确认测试过程中发现的软件缺陷或错误的策略,通常需要和用户充分协商。
软件配置复查
确认测试的一个重要内容是复查软件配置。复查的目的是保证软件配置的所有成分都齐全,质量符合要求,文档与程序完全一致,具有完成软件维护所必须的细节,而且已经编好目录。
除了按合同规定的内容和要求,由人工审查软件配置之外,在确认测试过程中还应该严格遵循用户指南及其他操作程序,以便检验这些使用手册的完整性和正确性。必须仔细记录发现的遗漏或错误,并且适当地补充和改正。
Alpha和Beta测试
如果软件是专为某个客户开发的,可以进行一系列验收测试,以便用户确认所有需求都得到满足了。验收测试是由最终用户而不是系统的开发者进行的。事实上,验收测试可以持续几个星期甚至几个月,因此能够发现随着时间流逝可能会降低系统质量的累积错误。
如果一个软件是为许多客户开发的(例如,向大众公开出售的盒装软件产品),那么,让每个客户都进行正式的验收测试是不现实的。在这种情况下,绝大多数软件开发商都使用被称为Alpha测试和Beta测试的过程,来发现那些看起来只有最终用户才能发现的错误。
Alpha测试由用户在开发者的场所进行,并且在开发者对用户的“指导”下进行测试。开发者负责记录发现的错误和使用中遇到的问题。总之,Alpha测试是在受控的环境中进行的。
Beta测试由软件的最终用户们在一个或多个客户场所进行。与Alpha测试不同,开发者通常不在Beta测试的现场,因此,Beta测试是软件在开发者不能控制的环境中的“真实”应用。用户记录在Beta测试过程中遇到的一切问题(真实的或想像的),并且定期把这些问题报告给开发者。接收到在Beta测试期间报告的问题之后,开发者对软件产品进行必要的修改,并准备向全体客户发布最终的软件产品。