在代码编写完成后的单元测试工作主要分为两个步骤人工静态检查和动态执行跟踪。<?xml:namespace prefix = o />
人工静态检查是测试的第一步,这个阶段工作主要是保证代码算法的逻辑正确性(尽量通过人工检查发现代码的逻辑错误)、清晰性、规范性、一致性、算法高效性。并尽可能的发现程序中没有发现的错误。
第二步是通过设计测试用例,执行待测程序来跟踪比较实际结果与预期结果来发现错误。经验表明,使用人工静态检查法能够有效的发现30%到70%的逻辑设计和编码错误。但是代码中仍会有大量的隐性错误无法通过视觉检查发现,必须通过跟踪调试法细心分析才能够捕捉到。所以,动态跟踪调试方法也成了单元测试的重点与难点。
(一)人工检查
通常在人工检查阶段必须执行以下项目的活动:
1. 检查算法的逻辑正确性;确定所编写的代码算法、数据结构定义(如:队列、堆栈等)是否实现了模块或方法所要求的功能。
2. 2. 模块接口的正确性检查;确定形式参数个数、数据类型、顺序是否正确;确定返回值类型及返回值的正确性。
3. 3. 输入参数有没有作正确性检查;如果没有作正确性检查,确定该参数是否的确无需做参数正确性检查,否则请添加上参数的正确性检查。经验表明,缺少参数正确性检查的代码是造成软件系统不稳定的主要原因之一。
4. 调用其他方法接口的正确性;检查实参类型正确与否、传入的参数值正确与否、个数正确与否,特别是具有多态的方法。返回值正确与否,有没有误解返回值所表示的意思。最好对每个被调用的方法的返回值用显湿代码作正确性检查,如果被调用方法出现异常或错误程序应该给予反馈,并添加适当的出错处理代码。
5. 5. 出错处理;模块代码要求能预见出错的条件,并设置适当的出错处理,以便在一旦程序出错时,能对出错程序重做安排,保证其逻辑的正确性,这种出错处理应当是模块功能的一部分。若出现下列情况之一,则表明模块的错误处理功能包含有错误或缺陷:出错的描述难以理解;出错的描述不足以对错误定位,不足以确定出错的原因;显示的错误信息与实际的错误原因不符;对错误条件的处理不正确;在对错误进行处理之前,错误条件已经引起系统的干预等。
6. 保证表达式、SQL语句的正确性;检查所编写的SQL语句的语法、逻辑的正确性。对表达式应该保证不含二义性,对于容易产生歧义的表达式或运算符优先级(如:《、=、 》、 &&、||、++、 --等)可以采用扩号“()”运算符避免二义性,这样一方面能够保证代码的正确可靠,同时也能够提高代码的可读性。
7. 7. 检查常量或全局变量使用的正确性;确定所使用的常量或全局变量的取值和数值、数据类型;保证常量每次引用同它的取值、数值和类型的一致性。
8. 8. 表示符合定义的规范一致性;保证变量命名能够见名知意,并且简洁但不宜过长或过短、规范、容易记忆、最好能够拼读。并尽量保证用相同的表示符代表相同功能,不要将不同的功能用相同的表示符表示;更不要用相同的表示符代表不同的功能意义。
9. 程序风格的一致性、规范性;代码必须能保证符合企业规范,保证所有成员的代码风格一致、规范、工整。例如对数组做循环,不要一会儿采用下标变量从下到上的方式(如:for(I=0;I++;I<10)),一会儿又采用从上到下的方式(如:for(I=10;I--;I>0));应该尽量采用统一的方式,或则统一从下到上,或则统一从上到下。建议采用for循环和While循环,不要采用do{}while循环等。
10.检查程序中使用到的神秘数字是否采用了表示符定义。神秘的数字包括各种常数、数组的大小、字符位置、变换因子以及程序中出现的其他以文字形式写出的数值。在程序源代码里,一个具有原本形式的数对其本身的重要性或作用没提供任何指示性信息,它们也导致程序难以理解和修改。对于这类神秘数字必须采用相应的标量来表示;如果该数字在整个系统中都可能使用到务必将它定义为全局常量;如果该神秘数字在一个类中使用可将其定义为类的属性(Attribute),如果该神秘数字只在一个方法中出现务必将其定义为局部变量或常量。
11. 检查代码是否可以优化、算法效率是否最高。如:SQL语句是否可以优化,是否可以用1条SQL语句代替程序中的多条SQL语句的功能,循环是否必要,循环中的语句是否可以抽出到循环之外等。
12 12. 检查您的程序是否清晰简洁容易理解。注意:冗长的程序并不一定不是清晰的。
13 13. 检查方法内部注释是否完整;是否清晰简洁;是否正确的反映了代码的功能,错误的注释比没有注释更糟;是否做了多余的注释;对于简单的一看就懂的代码没有必要注释。
注:检查注释文档是否完整;对包、类、属性、方法功能、参数、返回值的注释是否正确且容易理解;是否会落了或多了某个参数的注释,参数类型是否正确,参数的限定值是否正确。特别是对于形式参数与返回值中关于神秘数值的注释,如:类型参数应该指出“代表什么”。对于返回结果集(Result Set)的注释,应该注释结果集中包含那些字段及字段类型、字段顺序等。
(二)动态测试<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
单元白盒跟踪测试,通常需要做如下三项工作:
1. 设计测试用例;
2. 设计测试类模块;
3. 跟踪调试。
u 测试用例设计
通常动态执行跟踪调试是在编码阶段进行的。在对源程序作静态人工检查之后就可以开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。
1. 测试用例的设计基本原则
1)测试用例应由测试输入数据和与之对应的预期输出结果这两部分组成;
2)在测试用例设计时,应当包含合理的输入条件和不合理的输入条件。
2. 白盒测试的测试用例设计
白盒测试测试用例一般采用逻辑覆盖法和基本路径法进行设计。
逻辑覆盖法
逻辑覆盖是以程序内部的逻辑结构为基础的测试用例设计技术,这一方法要求测试人员对程序的逻辑结构有清楚的了解。逻辑覆盖可分为:语句覆盖、判定覆盖、条件覆盖、判定-条件覆盖、条件组合覆盖与路径覆盖。
1. 1. 语句覆盖就是设计若干个测试用例,运行所测程序,使得每一可执行语句至少执行一次。
2. 2. 判定覆盖就是设计若干个测试用例,运行所测程序,使得程序中每个判断的取真分支和取假分支至少经历一次。
3. 3. 条件覆盖就是设计若干个测试用例,运行所测程序,使得程序中每个判断的每个条件的可能取值至少执行一次。
4. 4. 判定--条件覆盖就是设计足够的测试用例,使得判断中每个条件的所有可能取值至少执行一次,同时每个判断的所有可能判断结果也至少执行一次。
5. 5. 条件组合覆盖就是设计足够的测试用例,运行所测程序,使得每个判断的所有可能的条件取值组合至少执行一次。
每一种覆盖方法都有其优缺点,这6种覆盖方法关系,如图1:
<?xml:namespace prefix = v />
通常在设计测试用例时应该根据代码模块的复杂度,选择覆盖方法。一般的代码的复杂度与测试用例设计的复杂度成正比。因此,设计人员必须做到模块或方法功能的单一性、高内聚性,使得方法或函数代码尽可能的简单;这样将可大大提高测试用例设计的容易度,提高测试用例的覆盖程度。
基本路径法
基本路径测试法是在程序控制流图的基础上,通过分析控制构造的环路复杂性,导出基本可执行路径集合,从而设计测试用例的方法。设计出的测试用例要保证在测试中程序的每个可执行语句至少执行一次。基本路径测试法包括以下5个方面:
1. 1.程序的控制流图:描述程序控制流的一种图示方法。
2. 2. 程序环境复杂性:McCabe复杂性度量;从程序的环路复杂性可导出程序基本路径集合中的独立路径条数,这是确定程序中每个可执行语句至少执行依次所必须的测试用例数目的上界。
3. 3. 导出测试用例。
4. 4. 准备测试用例,确保基本路径集中的每一条路径的执行。
采用边界值分析法设计合理的输入条件与不合理的输入条件;条件边界测试用例应该包括输入参数的边界与条件边界(if,while,for,switch ,SQL Where子句等)。
错误推测法
列举出程序中所有可能的错误和容易发生错误的特殊情况,根据它们选择测试用例.应着重测试以下方面:
1输出的出错信息难以理解;
2记录的错误与实际遇到的错误不相符;
3在程序自定义的出错处理段运行之前,系统已介入;
4异常处理不当;
5错误陈述中未能提供足够的定位出错信息。
一个模块或一个方法(Method)并不是一个独立的程序,在考虑测试它时要同时考虑它和外界的联系,用些辅助模块去模拟与所测模块相联系的其他模块。这些辅助模块分为两种:
1. 驱动模块(driver):相当于所测模块的主程序。它接收测试数据,把这些数据传送给所测模块,最后再输出实际测试结果。
2. 桩模块(stub):用于代替所测模块调用的子模块。桩模块可以做少量的数据操作,不需要把子模块所有功能都带进来,但不容许什么事情也不做。
所测模块与它相关的驱动模块及桩模块共同构成了一个“测试环境”,如图2。
图2 单元测试的测试环境
对于每一个包或子系统我们可以根据所编写的测试用例来编写一个测试模块类来做驱动模块,用于测试包中所有的待测试模块。而最好不要在每个类中用一个测试函数的方法,来测试跟踪类中所有的方法。这样的好处在于:
1. 能够同时测试包中所有的方法或模块,也可以方便的测试跟踪指定的模块或方法。
2. 能够联合使用所有测试用例对同一段代码执行测试,发现问题。
3. 便以回归测试,当某个模块作了修改之后,只要执行测试类就可以执行所有被测的模块或方法。这样不但能够方便得检查、跟踪所修改的代码,而且能够检查出修改对包内相关模块或方法所造成的影响,使修改引进的错误得以及时发现。
4. 复用测试方法,使测试单元保持持久性,并可以用既有的测试来编写相关测试。
5. 将测试代码与产品代码分开,使代码更清晰、简洁;提高测试代码与被测代码的可维护性。
u 跟踪调试
跟踪调试不但是深入测试代码的最佳方法,而且也是程序调试发现错误根源的有利工具。
下面介绍几种排错时应该采用的方法策略。
1.
断 断点设置,设置断点对源程序实行断点跟踪将能够大大提高排错的效率。通常断点的设置除了根据经验与错误信息来设置外,还应重点考虑以下几种类行的语句。
2. 函数调用语句。子函数的调用语句是测试的重点,一方面由于在调用子函数时可能引起接口引用错误,另一方面可能是子函数本身的错误。
3. 判定转移/循环语句。判定语句常常会由于边界值与比较优先级等问题引起错误或失效而作出错误的转移。因此,对于判定转移/循环语句也是一个重要的测试点。
4. SQL语句。对于数据库的应用程序来说,SQL语句常常会在模块中占比较重要的业务逻辑,而且比较复杂。因此,它也属于比较容易出现错误的语句。
5. 复杂算法段。出错的概率常与算法的复杂度成正比。所以越复杂的算法越需要作重点跟踪,如递归、回朔等算法。
6. 疑变量查看,在跟踪执行状态下当程序停止在某条语句时可以查看变量的当前值和对象的当前属性。通过对比这些变量当前值与预期值可以轻松的定位程序问题根源。
7. SQL语句执行检查,在跟踪执行或运行状态下将疑似错误的SQL语句打印出来,重新在数据库SQL查询分析器(如:Oracle SQL Plus)中跟踪执行可以较高效的检查纠正SQL语句错误。
8. 注意群集现象,经验表明测试后程序中残存的错误数目与该程序中已发现的错误数目或检错率成正比。根据这个规律,应当对错误群集的程序段进行重点测试,以提高测试投资的效益。如果发现某一代码段似乎比其他程序模块更多的错误倾向时,则应当花费较多的时间和代价测试这个程序模块。
l 模块接口测试
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。测试接口正确与否应该考虑下列因素:
1 输入的实际参数与形式参数的个数是否相同;
2 输入的实际参数与形式参数的属性是否匹配;
3 输入的实际参数与形式参数的量纲是否一致;
4 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
5 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
6调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
7 调用预定义函数时所用参数的个数、属性和次序是否正确;
8 是否存在与当前入口点无关的参数引用;
9 是否修改了只读型参数;
10 对全程变量的定义各模块是否一致;
11是否把某些约束作为参数传递。
如果模块内包括外部输入输出,还应该考虑下列因素:
1 文件属性是否正确;
2 OPEN/CLOSE语句是否正确;
3 格式说明与输入输出语句是否匹配;
4缓冲区大小与记录长度是否匹配;
5文件使用前是否已经打开;
6是否处理了文件尾;
7是否处理了输入/输出错误;
8输出信息中是否有文字性错误;
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
1 不合适或不相容的类型说明;
2变量无初值;
3变量初始化或省缺值有错;
4不正确的变量名(拼错或不正确地截断);
5出现上溢、下溢和地址异常。
除了局部数据结构外,如果可能,单元测试时还应该查清全局数据(例如FORTRAN的公用区)对模块的影响。
l 模块中所有独立执行通路测试
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。此时设计测试用例是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。此时基本路径测试和循环测试是最常用且最有效的测试技术。计算中常见的错误包括:
1 误解或用错了算符优先级;
2混合类型运算;
3变量初值错;
4精度不够;
5表达式符号错。
比较判断与控制流常常紧密相关,测试用例还应致力于发现下列错误:
1不同数据类型的对象之间进行比较;
2错误地使用逻辑运算符或优先级;
3因计算机表示的局限性,期望理论上相等而实际上不相等的两个量相等;
4比较运算或变量出错;
5循环终止条件或不可能出现;
6迭代发散时不能退出;
7错误地修改了循环变量。
l 静态测试
1. 变量定义是否正确(长度、类型、存储类型)
2. 子程序(函数和方法)接受的参数类型、大小、次序是否和调用模块相匹配合
3. 函数的返回值类型是否正确
4. 程序中是否引用了未初始化变量
5. 数组和字符串的下标是否为整数
6. 数组和字符串的下标是否在范围内(不“越界”)
7. 进行数组的检索及其它操作中,是否会出现“漏掉一个这种情况”
8. 是否在应该使用常量的地方使用了变量(例:数组范围检查)
9. 是否为变量赋予不同类型的值
10. (9)的情况下,赋值是否符合数据类型的转换规则
11. 变量的命名是否相似
12. 是否存在声明过,但从未引用或者只引用过一次的变量
13. 在特定模块中所有的变量是否都显式声明过
14. 非(14)的情况下,是否可以理解为该变量具有更高的共享级别
15. 是否为引用的指针分配内存
16. 数据结构在函数和子程序中的引用是否明确定义了其结构
17. 计算中是否使用了不同数据类型的变量
18. 计算中是否使用了不同的数据类型相同但长度不同的变量
19. 赋值的目的变量是否小于赋值表达式的值
20. 数值计算是否会出现溢出(向上)的情况
21. 数值计算是否会出现溢出(向下)的情况
22. 除数是否可能为零
23. 某些计算是否会丢失计算精度
24. 变量的值是否超过有意义的值
25. 计算式的求值的顺序是否容易让人感到混乱
26. 比较是否正确
27. 是否存在分数和浮点数的比较
28. 如果(28),精度问题是否会影响比较
29. 每一个逻辑表达式是否都得到了正确表达
30. 逻辑表达式的操作数是否均为逻辑值
31. 程序中的Begin…End和Do…While等语句中,End是否对应
32. 程序、模块、子程序和循环是否能够终止
33. 是否存在永不执行的循环
34. 是否存在多循环一次或少循环一次的情况
35. 循环变量是否在循环内被错误地修改
36. 多分支选择中,索引变量是否能超过可能的分支数
37. 如果(41),该情况是否能够得到正确处理
38. 全局变量定义和用法在各个模块中是否一致
39. 是否修改了只作为输入用的参数
40. 常量是否被作为形式参数进行传递
l 动态测试
1. 测试数据是否具有一定的代表性
2. 测试数据是否包含测试所用的各个等价类(边界条件、次边界条件、空白、无效)
3. 是否可能从客户那边得到测试数据
4. 非(3)的情况下,所用的测试数据是否具有实际的意义(客户业务上的)
5. 是否每一组测试数据都得到了执行
6. 每一组测试数据的测试结果是否与预期结果一致
7. 文件的属性是否正确
8. 打开文件语句是否正确
9. 输入/输出语句是否与格式说明书所记述的一致
10. 缓冲区大小与记录长度是否匹配
11. 使用文件前是否已打开了文件
12. 文件结束条件是否存在
13. 产生输入/输出错误时,系统是否进行检测并处理
14. 输出信息中是否存在文字书写错误和语法错误
15. 数字输入框是否接受数字输入
16. (15)的情况下、数字是否按既定格式显示
17. 数字输入框是否拒绝字符串和“非法”数字的输入
18. 组合框是否的能够进行下拉选择
19. 组合框是否能够进行下拉多项选择
20. 对于可添加数据组合框,添加数据后数据是否能够得到正确显示和进行选择
21. 列表框是否能够进行选择
22. 多项列表框是否能够进行多数据项选择
23. 日期输入框是否接受正确的日期输入
24. 日期输入框是否拒绝错误的日期输入
25. 日期输入框在日期输入后是否按既定的日期格式显示日期
26. 单选组内是否有且只有一个单选钮可选
27. 如果单选组内无单选钮可选,这种情况是否允许存在
28. 复选框组内是否允许多个复选框(包括全部可选)可选
29. 如果复选框组内无复选框可选,这种情况是否允许存在
30. 文本框及某些控件拒绝输入和选择时显示区域是否变灰或按既定规约处理
31. 文本框中数据格式(大小、对齐方向、颜色、背景)是否符合规范
32. 密码输入框是否按掩码的方式显示
33. 控件是否存在默认输入值,若存在,默认值是否得到显示和提交
34. Cancel之类的按钮按下后,控件中的数据是否清空复原或按既定规约处理
35. Submit之类的按钮按下后,数据是否得到提交或按既定规约处理
36. 异常信息表述是否正确
37. 软件是否按预期方式处理错误
38. 文件或外设不存在的情况下是否存在相应的错误处理
39. 软件是否严格的遵循外设的读写格式
40. 产生的文件和数据表的格式是否正确
41. 产生的文件和数据表的计算结果是否正确
42. 打印的报表是否符合既定的格式
43. 错误日志的表述是否正确
44. 错误日志的格式是否正确
三、使用工具进行单元测试java可以用Junit、php可以用phpunit、c/c++可以用cppunit进行单元测试。以phpunit为例说明:以一个例子说明。
例子的源代码:
<?php
class Account{
var $balance;
function Account($initialBalance=0){
$this->balance = $initialBalance;
}
function withdraw($amount){
$this->balance -= $amount;
}
function deposit($amount){
$this->balance += $amount;
}
function getBalance(){
return $this->balance;
}
function transferFrom(&$sourceAccount,$amount){
$sourceAccount->withdraw($amount);
$this->deposit($amount);
}
?>
操作步骤:
1.建一个测试类
测试类的名称规则:目标测试类+Test。如AccountTest。
它是一个由PhpUnit提供的TestCase的子类。在这个TestCase类中有2个基本的方法:setUp和tearDown。这2个方法的实现在父类中是空过程,必须由我们自己去重载。其中SetUp 用于进行AccountTest类的初始化处理。tearDown 则用于AccountTest类的清空处理
<?php
class AccountTester extends TestCase{
var $_ac1;
var $_ac2;
var $_ac3;
var $_ac4;
function AccountTester($name){
$this->TestCase($name); // call parent constructor
}
function setUp(){
$this->_ac1 = new Account(100); // data for testWithdraw
$this->_ac2 = new Account(20); // data for testDeposit
$this->_ac3 = new Account(30); // data for testTransferFrom
$this->_ac4 = new Account(50);
}
}
?>
2.编写测试代码
<?php
// Make a withdrawal of 25 units from _ac1.
// _ac1's initial balance is 100
function testWithdraw(){
$this->_ac1->withdraw(25);
$this->assert($this->_ac1->getBalance() == 75); // 100 - 25 = 75
}
// Make a deposit of 10 units into _ac2.
// _ac1's initial balance is 20
function testDeposit(){
$this->_ac2->deposit(10);
$this->assertEquals(30,$this->_ac2->getBalance()); //20 +10 = 30
}
// Tranfers 10 units from _ac3 to _ac4
// _ac3's initial balance is 30
// _ac4's initial balance is 50
function testTransferFrom(){
$this->_ac4->transferFrom(&$this->_ac3,10);
$this->assertEquals(20,$this->_ac3->getBalance(),"Source account balance incorrect"); // 30 - 10 = 20
$this->assertEquals(60,$this->_ac4->getBalance(),"Target account balance incorrect"); // 50 + 10 = 60
}
?>
3.运行测试过程。
建立一个runtest.php测试程序来运行所有的测试过程。
runtest.php源代码如下:
<?php
$tSuite = new TestSuite(); //creation of the test suite object 创建测试套件对象
$tSuite->addtest(new AccountTester("testWithdraw")); //Add inidividual tests
$tSuite->addtest(new AccountTester("testDeposit")); //加入专门测试方法。
$tSuite->addtest(new AccountTester("testTransferFrom"));
$res = new TextTestResult(); //Creation of the Result 建立一个测试结果类
$tSuite->run(&$res); //Run of the test 运行测试
$res->report(); //Print results 输出测试结果。
?>
程序说明:
首先创建测试套件对象tSuite,然后逐一加入专门测试方法,addtest方法的参数是测试方法的再创建测试报告对象,随之运行测试。测试发现错误的结果由TestResult类捕捉,TestResult可以定制一套text/html的错误报告。如果有必要你也可以自己编写输出部分。测试结果封装在TestResult类中,为了输出测试结果。我们采用了phpUnit提供的另外一个类TextTestResult类,它可以输出文本或超文本格式的报告。当然我们也可以自己定制一个新的TestResult的子类控制更复杂的输出格式。
4.提示和技巧
1)在编写一个新的测试套件完成之后,我们可以先引入一个小小的bug以证明测试套件可以正常运行。
比如,在本例account类中,我们故意引入一个有问题的函数。
<?php
function withdraw($amount){
$this->balance -= $Amount;
// 变量名大小写错误,本意是调用$amount参数,结果引入一个新变量$Amount。
}
?>
好,现在让我们运行测试套件,如果不出意外的话,我们将很快发现错误之处。
2)要指出的是,并非所有的方法都需要测试。你只需对相关的方法进行测试。
3)如果在开始编码前就写好测试代码,会使你更进一步明白你的程序到底需要完成什么样的任务。
5.测试结果
1)测试通过的结果
PHPUnit 2.1.4 by Sebastian Bergmann.
Time: 0.115653991699
OK (1 test)
2)如果测试失败,则显示具体的失败测试