UML软件工程组织

 

 

经典之作《代码大全》中的单元测试
 
作者:Steve McConnell 文章来源:网络
 

单元测试在软件质量中的作用

 测试对任何一个软件质量来说是重要的,在许多场合,它只是其中一部分。这是不幸的,因为评审的各种方式证明,它们比测试要能发现更多的错误,而且评审发现每个错误所花费仅为测试的一半左右(Card 1987)。单个测试方法(单元测试、功能测试、部分测试、系统测试),通常只能发现少于 50%的错误数。几种测试方法的集成使用,只会发现少于 60%的错误(Jones1986)。然而,由于测试已被广泛使用,而且它比评审要能发现更多种类型的错误,因此它应该测试对任何一个软件质量来说是重要的,在许多场合,它只是其中一部分。这是不幸的,因为评审的各种方式证明,它们比测试要能发现更多的错误,而且评审发现每个错误所花费仅为测试的一半左右(Card 1987)。单个测试方法(单元测试、功能测试、部分测试、系统测试),通常只能发现少于 50%的错误数。几种测试方法的集成使用,只会发现少于 60%的错误(Jones1986)。然而,由于测试已被广泛使用,而且它比评审要能发现更多种类型的错误,因此它应该较好的。当然如果了解盒子的内部情况,你就可更仔细地测试你的子程序,至少你有更多的机会发现错误。和不熟悉代码者相比较,你处在发现错误的更有利位置上。在创建过程中,你编写子程序,对其进行手工检查,然后评审或测试它。不论你的集成或系统测试策略如何,在你将其合并之前,你应仔细地测试每个单元。如果你在编写几个子程序,你应不时测试一下它们。单个测试子程序并不容易,但是要调试它们是容易的。如果你马上将未测试的子程序组合起来并且发现了错误,那么其中任何一个子程序都有可能有错误。如果你
每次将一个子程序加进已经测试过的子程序中,你就能知道任何一个新错误可能是由新的子程序或者是由新、旧子程序的相互作用所引起的。这时,使用调试方法较为简单。
 测试技巧

 为什么有可能利用测试确定程序中的错误呢?为了测试程序的性能,你不得不对你的程序测试每一种输入数据或输入数据的组合。即使对一个简单的程序,这样的工作也令人难以容忍。

 例如,你的程序接收人名、地址、电话号码并将其存在一个文件中。这是一个简单的程序,并且比任何让你厌烦的程序都要简单。进一步假定每个可能的名字和地址是 20 个字符的长度,并且它们要用到 26 个可能的字符。以下是可能的输入数据量:

 名字 2620(20 个字符,每个字符有 26 种可能选择)
 地址 2620(20 个字符,每个字符有 26 种可能选择)
 电话号码 1010(10 个数字,每个数字有 10 种可能选择)
 总的可能 =2620*2620*1010=1066

 即使是这样较小输入量,你也将有 1066 种测试用例。如果诺亚走出其方舟并以每秒 1 兆次的速度对程序进行测试,他即使到现在也才完成了全部工作量的 l%。显然,如果你增大实际数据量,对全部可能性进行测试几乎是不可能的。

 不完全测试

 对各种可能全部进行测试是不可能的,实际说来,测试的艺术在于从所有测试用例中找出最能发现错误的示例来。在 10 种可能测试示例中,只有少部分可能发现错误。你应侧重于从所有测试示例中找出能提示不同点的用例,而不是那些不断地重复着的用例。

 当你计划测试时,你应排除那些没有告诉你任何新东西的用例,这就是说,此时对新数据的测试将不会产生错误。 人们已提出了不少有效的非实例测试法,下文将讨论其中的一些方法。

 善于结构的测试

 尽管其名字是粗糙的,基于结构的测试,其实是一个简单概念。其意思是你应对你程序中的每一条语句至少测试一次,如果本语句是一条逻辑语句,通常是用 if 或 while,你就应根据if 或 while 表达式中的复杂性仔细测试,这样才能确保每条语句都经过了测试。确保所有语句都经过测试的最简单的方法是由程序计算路径数,然后设计最少数量的测试用例,以确保所有路径都得到了测试。你可能听说过“代码覆盖”测试或“逻辑覆盖”测试;它们都是使你的测试程序中所有路径的方法。由于这两种方法覆盖了所有路径,它们和基于结构的测试是相似的,但是这二种方法覆盖所有路径时并不使用最小的测试用例集。如果你使用代码覆盖或逻辑覆盖方法,你所需的测试用例可能比你用基于结构的测试所需的测试用例要多。

 你可用表 25-1 所给出的方法计算所需的最少测试用例数。表 25-l 确定基于结构的测试方法所需的测试用例

 1.由程序的第一条直接路径开始,设定计数值为 1。
 2.每遇到下一个关键词或其等价词: if,while,repeat,for,and 和 or 计数值加 1。
 3.在 case 语句中每遇到一个 case, if 数值加 1。如果 case 语句中不含缺省语句,计数值再加 1。

 以下是一个例子:
 计算路径数目的一个简单的 Pascal 程序例子:
 Statement1; ――开始计数 1
 Statement2;
 if X < 10 then ――遇到 if 计数 2
 begin
 Statement3;
 End;1
 Statement4;
 本例中,你一开始将计数置为 1,在遇到 if 语句后计数值变为 2,这意味着你至少需要 2
 个用例以便覆盖程序中的所有路径。在以上例子中,你应有以下示例:
 · 受 if 控制的语句得到执行(X<10)
 · 不受 if 控制的语句得到执行:(X>=10)
 代码例子还应更实际一点,以便对测试工作有一个清晰的了解。例子中实际还包括有缺陷的代码。

 下面的程序稍复杂一点。在本章以后都使用这个例子并且它可能有一些错误。

 确定基于结构的测试所需测试用例的一个 Pascal 例子;

1 { Compute Net Pay} 程序开始,计数 1
2
3 TtlWithholdings := 0;
4
5 for ID := 1 to NumEInPloyees ——for,计数 2
6 begin
7
8 { compute social security withholding, if below the maximum }
9 if(Employee[ID].SSWithheld<MAX_SOCIAL_SECURITY )then ——if,计数 3
10 begin
11 SocialSecurity := ComputeSocialSecurity(Employee[ID])
12 end;
13
14 {set default to no retirement contribution}
15 Retirement := 0;
16
17 {determine discretionary employee retirement contribution}
18 if(Employee[ID].WantsRetirement)and ——if,计数 4 and,计数 5
19 (EligibleFotRetirement( Employee[ID])) then
20 begin
21 Retirement := GetRetirement(Employee[ID]);
22 end;
23
24 GrossPay := ComputeGrossPay(Employee[ID]);

25
26 { determine IRA contribution }
27 IRA := 0;
28 if( EligibleForIRA( EmPloyee[ID]) ) then ——if,计数 6
29 begin
30 IRA := IRAContribution(Employee[ID],Retirement,GrossPay)
31 end;
32
33 { make weekly paycheck}
34 Withholding := ComputeWithholding(Employee[ID]);
35 NetPay := GrossPay-Withholding-Retirement-
36 SocialSecurity-IRA;
37 PayEmployee(EmPloyee[ID],NetPay);
38
39 { add this employee’s paycheck to total for accounting}
40 TtlWithholdings := TtlWithholdingst 十 Withholding;
41 TtlSocialb

ourity := TtlSocialSecurity +SocialSecurity;
42 TtlRetirement := TtlRetirement + Retirement;
43 end; {for}
44
45 SavePayRecords( Ttlwithholdings, Ttlsocialsecurity, TtlRetirement);

本例中,你需要有一个初始计数值,每遇到 5 个关键词的一个,计数值加 1。但是并不意味着任意 6 个测试用例就覆盖所有示例。它只是表明最少需要 6 个用例。除非这些用例构造得相当好,否则将不会覆盖所有情况。关键是当你计算所需用例的数目时,你应注意你所用的相同的关键词。代码中每个关键词代表了或真或假的一类事物。你能确信对每个真或假应至少有一个测试用例与其—一对应。

 以下是覆盖了上例中所有基数的测试用例:

用例 测试描述 测试数据

  • 1. 无用例 所有布尔值为真
  • 2. 初始 for 条件为假 NumEmployees<1
  • 3. 第一个 if 为假 Employee[ID]SSWithheld>=MAX_SOCIAL_SECURITY
  • 4. 第二个 if 为假(因为 and 的第一部分为假) not Employee[ID].WantsRetirement
  • 5. 第二个 if 为假(因为 not EligilbleForRetirement(Employee[ID])and 的第二部分为假)
  • 6. 第三个 if 为假 not ElgibleForIRA(Employee[ID])

如果你的子程序比以上所讨论程序还要复杂,你所用测试用例以便覆盖所有路径的数目将会大大增加。短的子程序有较小的测试路径。没有较多 and 和 or 的布尔表达式需测试的变量数也较少。测试的容易也正说明了子程序较短并且你的布尔表达式较为简单。

 既然已为你的子程序创建了 6 个测试用例,并且满足了基于结构的测试需求,你是否以为如果你的子程序比以上所讨论程序还要复杂,你所用测试用例以便覆盖所有路径的数目将会大大增加。短的子程序有较小的测试路径。没有较多 and 和 or 的布尔表达式需测试的变量数也较少。测试的容易也正说明了子程序较短并且你的布尔表达式较为简单。

 既然已为你的子程序创建了 6 个测试用例,并且满足了基于结构的测试需求,你是否以为· 所有定义。对每个变量的所有定义进行测试(这就是说,任何一个变量每接收一个数据时都应对其测试)。这是不好的策略,因为如果你想测试代码的每一行的话,你可能会失败。

 · 所有定义——使用混合数据状态。测试每一个某处定义然后在另一处使用的变量。这较测试所有定义是一种较好的策略,因为它仅执行每一行代码,而不确保每一定义——使用类型的数据将接受测试。以下是一个例子:

 将接受数据流测试的一个程序例子:

if (Condition 1)
x = a;
else
x = b;
if (Condition 2)
y = x + 1;
else
y = x – 1;

为了覆盖程序中的所有路径,你需要使条件 1 是真或假的测试用例。你也需要使条件 2 为真或假的测试用例。以上可以用二个测试用例来处理:用例 1(条件 1 为真,条件 2 为真)和用例2(条件 1 为假而条件 2 为假)。以上二个用例是你用基于结构的测试方法所必需的。它们也是你执行定义变量的每一行代码所必需的;它们可自动地进行简单的数据流测试。
为了覆盖每一种定义一使用类型,你需增加一些测试用例。你应还有使条件 1 和条件 2 同为真或假的测试用例:

x = a;
??
y = x + 1;
and
x = b;
y = x – 1;

但是你还是需要更多的用例以测试定义——使用混合类型的数据。你需要: (1)x=a 然后y=x-1; (2)x=b 然后 y=x+1。在本例中,你可增加 2 个用例而得到以上组合:用例 3(条件 1 为真,条件 2 为假)和用例 4(条件 1 为假条件 2 为真)。开发测试用例的较好方法是从基于结构化的测试开始,它可向你提供一些定义使用数据流。

 然后增加一些测试用例以便得到完整的定义使用数据流测试用例。正如上一节所讨论的那样,基于结构的测试给第 25.3 节中的一个 45 行的 Pascal 语言程序提供了 6 个测试用例。对每个定义一使用类型的数据流进行测试需要有更多的用例。可能其中一些被一些已存在的测试用例所覆盖。以下是在基于结构的测试所产生的测试用例上所增加的数据流混合类型用例。

 用例 测试描述

 7 在第 15 行中定义在第 30 行第一次使用,且没有被以前其它测试用例覆盖。
 8 在第 15 行中定义在第 35 行第一次使用,且没有被以前其它测试用例所覆盖。
 9 在第 21 行中定义在第 35 行第一次使用,且没有被以前其它测试用例所覆盖。

 当你做过几次数据流测试用例后,你就能明白哪些测试用例是颇有成效的,哪些是已被覆盖了的。当你测试受阻时,你可列出所有的定义一使用混合类型数据。虽然看起来可能费事,但是,它可使你明白一些用基于结构测试的方法所不能得到的用例。

 等效类划分

 一个好的测试用例应覆盖相当大一部分可能的数据输入。如果二个测试用例提示出相同的错误,你可挑选其中的任一个。“等效类划分”的概念是以上思想的体系化并可减少所需的测试用例数。

 本书第 25.3 节的 45 行 Pascal 程序中,第 9 行是使用等效类划分的好地方。测试条件是Employee[ID].SSWithheld < MAX_SOCIAL_SECURITY。此时测试用例有二类:第一类是Employee[ID].SSWithheld 比 MAX_SOCIAL_SECURITY 小,第二类是 Employee[ID].SSWithheld大于或等于 MAX_SOCIAL_SECURITY。程序的其它部分可能有其它的等效类,这意味着你可能将测试比Employee[ID].SSWithheld 二个值更多的用例,但是对于我们所讨论的这个程序来说,只需讨论 2 个即可。

 当你已经对程序进行了基本和数据流测试时,再进行等效类划分将不会使你对程序有深入的认识。当你从子程序的外部看它(如从描述而不是从源代码时),或数据较为复杂而这种复杂并没有在程序的逻辑结构中有所反应时,等效类划分是异常有用的。

 错误猜测
 
 除了使用正规的测试技术外,好的程序员常使用一些非正规的、直接推断方法以提示代码

中的错误。对应用编程来说,测试方法的不同往往导致测试结果的不同。例外情况是实时处理,但它不在本文的讨论范围内。其中的一个直接推断方法是错误猜测。“错误猜测”这个词是对一个合理的概念所取的平庸的名字。错误猜测意味着测试用例建筑在对程序可能发生错误处的猜测上,错误猜测是需要一定经验的。

 你对错误的猜测是建筑在直觉或过去的经验上。第 24 章指出检查的一个优点是它们能指出和列出常见错误表。错误表可用来检查新的代码。当你将过去所遇到的错误记录下来,你就有可能增大你的错误猜测所发现错误的可能性。以下几部分讨论了几种可进行错误猜测的错误类型。

 边界分析

 测试的一个最有收效的领域是边界条件仅发生微妙的错误,如将 Num – 1 值认为是 Num的值。或将 >= 误为 > 。

边界分析就是写出测试边界条件的用例来。如果你正在调试小于 MAX 范围内的数,你有以下三种可能条件,显示如下:


 正如上图所示,有三种可能的边界用例:小于最大值,最大值自身,以及大于最大值。一般需要以上三个测试用例,以便确保没有发生常见的错误。

 在 25.3 节中所示 45 行的 Pascal 程序包含着这样一个测试:Employee[ID].SSWithheld >MAX_SOCIAL_SECURITY,根据边界分析的原则,需检查三个用例:

 用例 测试描述

 1 定义用例 1 是使 Employee[ ID].SSWithheld<MAX_SOCIAL_SECURITY 这个条件为真时边界条件为真。于是,用例 1 使 Employee[ID].SSWithheld 为 MAX_SOCIAL_SECURITY – 1 值。本测试用例已经产生。

 2 定义用例 3 是使布尔条件 Employee[ID].SSWithheld<MAX_SOCIAL_SECURITY 为假时,边界条件为假。于是.用例了使 Employee[ID].SSWithheld 之值为 MAX_SOCIAL_SECURITY+l 本测试用例也已经产生。

 3 另 外 一 个 测 试 事 件 是 测 试 Employee[ID].SSWithheld=MAX_SOCLAL_SECURITY 这 是 个 死 循环事件。

 复合边界

 边界条件分析也同样存在最少和最大容许值。在本例中,它可能是最小或最大总开支、总收入或总贡献,但是由于对这些值的计算已超出子程序的范围,对它们的测试用例在此不作深入探讨。

 当边界含有几个变量时,一种更微妙的边界条件将从中产生。例如,二个变量相乘,当 2个数都是大正数时将会出现什么情况呢?大的负数呢?0 呢?如果传递给一个子程序的字符串都是非同一般的呢?在第 25.3 节的 45 行 Pascal 程序,当每个雇员的工资数相当多时——如一帮年薪为 250,000 美元的程序员(我们总是这样希望的),此时你会想到变量 TtlWitholdings,TtlSocialsecurity 和 TtlRetirement 到底有多大吗?以上是需要另外一个测试用例的:

 用例 测试描述
 11 现有一大帮雇员,每个人的薪水相当高——由于某种原因,1000 位雇员每人年薪为 250,000 美元,他们不需交任何社会保险机井区所有人都扣除退休保险金。以下是一个测试用例,但是和以上用例相反雇员人数少而且每个人的薪水为 0.00 美元。

 用例 测试描述
 12 10 位雇员,每人年薪为 0.00 美元

 坏数据的排序
 除了猜测边界条件时所发生的错误外,你也可猜测和测试几种其它类型的坏数据。典型的
坏数据测试用例包括:

正如上图所示,有三种可能的边界用例:小于最大值,最大值自身,以及大于最大值。一般需要以上三个测试用例,以便确保没有发生常见的错误。

 在 25.3 节中所示 45 行的 Pascal 程序包含着这样一个测试:Employee[ID].SSWithheld >MAX_SOCIAL_SECURITY,根据边界分析的原则,需检查三个用例:

 用例 测试描述 

 1 定义用例 1 是使 Employee[ ID].SSWithheld<MAX_SOCIAL_SECURITY 这个条件为真时边界条件为真。于是,用例 1 使 Employee[ID].SSWithheld 为 MAX_SOCIAL_SECURITY – 1 值。本测试用例已经产生。

 3 定义用例 3 是使布尔条件 Employee[ID].SSWithheld<MAX_SOCIAL_SECURITY 为假时,边界条件为假。于是.用例了使 Employee[ID].SSWithheld 之值为 MAX_SOCIAL_SECURITY+l 本测试用例也已经产生。

 10 另 外 一 个 测 试 事 件 是 测 试 Employee[ID].SSWithheld=MAX_SOCLAL_SECURITY 这是个死循环事件。

 复合边界
 
 边界条件分析也同样存在最少和最大容许值。在本例中,它可能是最小或最大总开支、总收入或总贡献,但是由于对这些值的计算已超出子程序的范围,对它们的测试用例在此不作深入探讨。

 当边界含有几个变量时,一种更微妙的边界条件将从中产生。例如,二个变量相乘,当 2个数都是大正数时将会出现什么情况呢?大的负数呢?0 呢?如果传递给一个子程序的字符串都是非同一般的呢?在第 25.3 节的 45 行 Pascal 程序,当每个雇员的工资数相当多时——如一帮年薪为 250,000 美元的程序员(我们总是这样希望的),此时你会想到变量 TtlWitholdings,TtlSocialsecurity 和 TtlRetirement 到底有多大吗?以上是需要另外一个测试用例的:

 用例 测试描述
 现有一大帮雇员,每个人的薪水相当高——由于某种原因,1000 位雇员每人年薪为 250,000 美元,他们不需交任何社会保险机井区所有人都扣除退休保险金。

 以下是一个测试用例,但是和以上用例相反雇员人数少而且每个人的薪水为 0.00 美元
 
 用例 测试描述

12 10 位雇员,每人年薪为 0.00 美元

  坏数据的排序

 除了猜测边界条件时所发生的错误外,你也可猜测和测试几种其它类型的坏数据。典型的
坏数据测试用例包括:

检查表
 测试用例

· 每个子程序的要求是否有自己的测试用例?
· 子程序结构的每个部分是否都有自己的测试用例?
· 程序中每一行代码都是否至少被一个测试用例所测试过?这是否是由通过计算测试
每一行代码所需的最少用例来确定的?
· 所有定义——使用数据流路径是否被至少一个测试用例所测试过?
· 代码是否被看起来不大正确的数据流模式所检查过?比如定义一定义,定义一退出,
和定义一失效?
· 是否使用常见错误表以便编写测试用例来发现过去常出现的错误?
· 是否所有的简单边界都得到了测试:最大、最小或易混淆边界?
· 是否所有复合边界都得到了测试?
· 是否对各种错误类型的数据都进行了测试?
· 是否所有典型的中间数都得到了测试?
· 是否对最小正常配置进行了测试?
· 是否对最大正常配置进行了测试?
· 是否测试了和旧数据的兼容性?是否所有保留下来的硬件、操作系统旧的版本,以及
其它软件旧版本的接口都得到了测试?
· 测试用例是否便于手工检查?

使用便于手工检查的用例

 假定你为某一工资系统编写测试用例,你需输入某人的薪水,其方法是你可随意敲进几个数,试敲入:1239078382346
这是一个相当高的薪水,比 1 万亿美元还要多,你可截短一部分得到一个更为实际的数字:39,078.38 美元。

 现在,进一步假定本测试用例成功了,就是说,发现了错误。你怎样知道这是一个错误?现在,再假定你知道答案,因为你用手工计算出了正确的答案。当你用一个奇怪的数字如34,078.38 美元进行计算时,你在得到程序结果的同时,自己却将结果计算错了。另一方面,一个较好的数字如 20,000 美元你一眼就能记住它。0 非常容易送入计算机,对 2 相乘是绝大多数程序员轻而易举就能完成的。

 你可能认为一些令人厌烦的数据如 39,078.73 美元可能更易提示出错误,但是它确实是不如其它数有效。

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号