第10章 单元测试
单元测试(unit testing)通常由开发人员完成,少部分公司中由测试人员完成。要实施单元测试,需要搭建单元测试环境,并能根据详细设计说明书来编写驱动和桩。
10.1 什么是单元测试
不同的测试阶段(单元测试、集成测试、系统测试、验收测试)能测试的点是有区别的,单元测试更多的是发现代码逻辑上的错误。
10.1.1 单元测试的概念
单元测试是对软件基本组成单元进行的测试,如函数(function或procedure)或一个类的方法(method)。这里的单元就是软件设计的最小单位。软件系统的结构如图10-1所示。在软件系统中,单元具有一些基本属性,如明确的功能、规格定义、与其他部分的接口定义等,可清晰地与同一程序的其他单元划分开来。
图10-1 软件系统结构
在传统的结构化编程语言(如C)中,要进行测试的基本单元一般是函数或子过程;在Java这样的面向对象的语言中,要进行测试的基本单元是类或类的方法。
这里的基本单元不一定是指一个具体的函数或一个类的方法,在具体实现时,也可能对应的是多个程序文件中的多个方法。例如,如果某个方法A只被方法B调用,并且方法A和方法B的代码在一定的范围之内,则可以考虑把A和B合并作为一个单元进行测试,但这些原则必须在单元测试计划或单元测试方案中明确说明。在纯Java语言的代码中,一般认为一个方法就是一个单元,这主要是为了避免开发人员和测试人员陷入不必要的单元争论当中,同时也可以避免歧义。
10.1.2 单元测试的目的
单元测试用于发现各模块内部可能存在的各种错误。单元测试主要基于白盒测试。
软件产品不仅包含代码,还包括各种文档。因此,单元测试应该从3个角度来考虑。
●针对文档的测试。
●针对代码的测试。
●针对文档和代码是否一致的测试。
在单元测试阶段,对应的文档是详细设计说明书,对应的代码就是单元代码,因此单元测试的目的主要有3方面。
●验证单元代码和详细设计说明书的一致性。
●跟踪详细设计说明书中设计的实现,发现详细设计说明书中存在的错误。因为测试分析和测试用例设计需要依据详细设计说明书来进行,这个过程实际上是对详细设计说明书的重新检视,在这个过程中会发现以前评审中没有发现的问题。
●发现在编码过程中引入的错误。
例如,设计一个方法abs(x),对x求绝对值,如图10-2所示。
图10-2 abs(x)的流程图
代码如下:
public int abs(int x) { if(x>=0) return x; else return -x; } |
如果把x≥0写成了x≤0,就出现了与设计不相符的错误。
10.1.3 单元的常见错误
单元的常见错误一般出现在5个方面,如图10-3所示,因此这5个方面是单元测试应该关注的重点。
图10-3 单元的常见错误
1.单元接口
单元接口是容易被忽略的地方,如果数据不能正确地输入和输出,就谈不上进行其他测试。因此,首先需要检查单元接口是否出现以下错误。
(1)被测单元的输入/输出参数在个数、属性、顺序上和详细设计说明书中的描述不一致。
① 个数错误。例如,方法定义了3个参数,但是在调用时函数调用传递了两个参数。
定义方法的代码如下:
public void addGoods(String name,int price,int num) { } |
调用方法的代码如下:
addGoods("iphoneX",6888); |
函数调用中传送了两个参数。
② 属性错误,例如,标准函数sin(x)中x是弧度,要注意转换,计算47°的正弦值应写成sin(PI*47/180)。
③ 顺序错误。例如,定义方法时3个参数的顺序是name、price、num,调用时写成了price、name、num。
定义方法的代码如下:
public void addGoods(String name,int price,int num) { } |
调用方法的代码如下:
addGoods(6888,"iphoneX",10); |
函数调用中传送了3个参数,但顺序错了。
(2)若修改了只做输入用的形参,可能会导致数据的错误修改。
例如,以下代码中修改了形参sum的值。
public void sumData(int num,int data[],int sum) { int i; for (i = 0; i < num; i++) { sum += data[i]; } } |
若改为如下代码,则更好。
public void sumData(int num,int data[],int sum) { int i; int sumTemp = 0; for (i = 0; i < num; i++) { sumTemp += data[i]; } sum=sumTemp; } |
这样可以防止返回的sum值在执行过程中被其他方法引用,导致数据不一致。
(3)若约束条件通过形参传送,就会导致方法间的耦合性增大。
耦合是指两个实体相互依赖对方的一个量度,分为以下几种。
●非直接耦合:两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。
●数据耦合:当一个模块访问另一个模块时,彼此之间是通过简单的数据参数(不是控制参数、公共数据结构或外部变量)来交换输入/输出信息的。
●标记耦合:一组模块通过参数表传递记录的信息。这个记录是某个数据结构的子结构,而不是简单变量。
●控制耦合:一个模块通过传送开关、标志、名字等控制信息明显地控制选择另一模块的功能。
●外部耦合:一组模块都访问同一个全局变量,而不是同一个全局数据结构,而且不通过参数表传递该全局变量的信息。
●公共耦合:当一组模块访问同一个公共数据环境时它们之间的耦合。公共的数据环境可以是全局数据结构共享的通信区、内存的公共覆盖区等。
●内容耦合:如果发生下列情形,两个模块之间就发生了内容耦合。
■ 一个模块直接访问另一个模块的内部数据。
■ 一个模块不通过正常入口转到另一模块内部。
■ 两个模块有一部分程序代码重叠(只可能出现在汇编语言中)。
■ 一个模块有多个入口。
不把约束条件作为形参传递的目的是防止方法间的控制耦合。调度方法是指根据输入的消息类型或控制命令来启动相应的功能实体(方法或过程),而本身并不完成具体功能。控制参数是指改变方法功能、行为的参数,即方法要根据此参数来决定具体怎样工作。非调度方法的控制参数增加了方法间的控制耦合。
例如,如下方法的构造不太合理。
public final char INTEGER_ADD = 'y'; public int add_sub(int a,int b,char addSubFlg) { if (addSubFlg == INTEGER_ADD) { return (a + b); } else { return (a -b); } } |
要避免把多种功能糅合在一起,代码越复杂,出现错误的可能性就越大。上述方法分为如下两个方法之后更清晰。
public int add( int a,int b ) { return (a + b); } public int sub( int a,int b ) { return (a–b); } |
以上是编码规则中的可选规则。
一个方法完成一个具体的功能。一般来说,一个方法中的代码最好不要超过600行,越少越好,一般介于100~300行。有证据表明,一个方法中的代码如果超过500行,就会有和其他方法相同或相近的代码,这样可以再写另一个方法。另外,一个方法一般完成一个特定的功能,禁止在一个方法中做许多件不同的事。方法的功能越单一越好,这不仅有利于提高方法的易读性,还有利于代码的维护和重用。
版权声明:51Testing软件测试网获得人民邮电出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。