白盒测试实例之一——需求说明 三角形的问题在很多软件测试的书籍中都出现过,问题虽小,五脏俱全,是个很不错的软件测试的教学例子。本文借助这个例子结合教学经验,从更高的视角来探讨需求分析、软件设计、软件开发与软件测试之间的关系与作用。 题目:根据下面给出的三角形的需求完成程序并完成测试: 一、输入条件: 1、 条件1:a+b>c 2、 条件2:a+c>b 3、 条件3:b+c>a 4、 条件4:0<a<200 5、 条件5:0<b<200 6、 条件6:0<c<200 7、 条件7:a==b 8、 条件8:a==c 9、 条件9:b==c 10、条件10:a2+b2==c2 11、条件11:a2+ c2== b2 12、条件12:c2+b2== a2 二、输出结果: 1、不能组成三角形 2、等边三角形 3、等腰三角形 4、直角三角形 5、一般三角形 6、某些边不满足限制 白盒测试实例之二——答案 很多初学者一看到这个需求(详见白盒测试实例之一——需求说明收藏),都觉得很简单,然后立刻就开始动手写代码了,这并不是一个很好的习惯。如果你的第一直觉也是这样的,不妨耐心看到文章的最后。 大部分人的思路: 1、首先建立一个main函数, main函数第一件事是提示用户输入三角形的三边,然后获取用户的输入(假设用户的输入都是整数的情况),用C语言来写,这一步基本上不是问题(printf和scanf),但是要求用java来写的话,很多学生就马上遇到问题了,java5.0及之前的版本不容易获取用户的输入。 点评:这样的思路做出来的程序只能通过手工方式来测试所有业务逻辑,而且这个程序只能是DOS界面版本了,要是想使用图形化界面来做输入,就得全部写过代码。 2、业务处理流程的思路用流程图表示如下: 3、C语言代码: | #include<stdio.h> void main() { int a, b, c; printf("please enter three integer:"); scanf("%d%d%d", &a, &b, &c); if(0<a && a<200 && 0<b && b<200 && 0<c && c<200) { if(a+b>c && a+c>b && c+b>a) { if(a==b && b==c && a==c) //这里可以省掉一个判断 { printf("1是等边三角形"); } else { if(a==b || b==c || a==c) { printf("2是等腰三角形"); } else { if(a*a+b*b==c*c || a*a+c*c==b*b || b*b+c*c==a*a) { printf("3是直角三角形"); } else { printf("4是一般三角形"); } } } } else { printf("5不能组成三角形"); } } else { printf("6某些边不满足限制"); } } |
点评:这样的思路做出来的程序只能通过手工方式来测试所有业务逻辑,而且这个程序只能是DOS界面版本了,要是想使用web或图形化界面来做输入,就得全部写过代码。 需求分析是后续工作的基石,如果分析思路有问题,后续工作可能就会走向不正确的方向,比如:代码重用性差、难于测试、难于扩展和难于维护等。反而,如果需求分析做的好,对设计、开发和测试来说,都可能是很大的帮助。 看到题目给出的条件达12个之多,粗粗一看,好像很复杂,但仔细分析之后,发现可以把它们分成4组来讨论: 1、 条件1:a+b>c; 条件2:a+c>b; 条件3:b+c>a 这三个表达式有什么特点呢?实际上它们的逻辑是一样的:两个数之和大于第三个数。那么,前面程序的写法就存在逻辑重复的地方,应该把这个逻辑提取到一个函数中。 2、 条件4:0<a<200; 条件5:0<b<200; 条件6:0<c<200 这三个表达式也是同一个逻辑:判断一个数的范围是否在(0, 200)区间内,也应该把这个逻辑提取到一个函数中,去掉重复的逻辑,提高代码的可重用性。 可重用性的好处:比如,现在用户的需求改为了三条边的取值范围要改为[100,400],那么,按前面的思路来说,需要改3个地方,而现在只需要在一个函数里改1个地方,这就是代码重用的好处。 3、条件7:a==b; 条件8:a==c; 条件9:b==c 这三个表达式的逻辑:判断两个数是否相等。也应该把它提取到一个函数中。 我们进一步来分析一下判断是否是等边三角形或等腰三角形的条件: (1)前面程序的判断是从最直观的方式(a==b && b==c && a==c)(实际上只需要两个表达式成立即可)三条边都相等来判定是等边三角形;(a==b || b==c || a==c)只有两条边相等来判定是等腰三角形。 (2)转变一下思路:给定三个整数,然后用一个函数来判断这三个整数有几个相等,返回相等的个数,如果返回值等于3,那么它是等边三角形,如果返回值是2,那么它是等腰三角形,否则,它是一般三角形(如果不是直角三角形的话)。 4、条件10:a2+b2==c2 条件11:a2+ c2== b2 条件12:c2+b2== a2 这三个条件的处理方式有两种: (1)跟前面三组分析一样,把相同的逻辑提取到一个函数中,然后三次调用。 (2)根据直角三角形的特点:斜边是最长的,所以我们可以事先写一个函数来找到最长的边,然后把它赋值给c,这样处理之后,只需要一次调用判定(a2+b2==c2)的函数了。 关键字:白盒测试 程序设计对于软件的质量和软件实施过程的难易程度起着至关重要的作用。好的设计,即使聘用没什么经验的开发人员都很容易产生出高质量的代码出来;而差的设计,即使是经验很丰富的开发人员也很容易产生缺陷,特别是可重用性、可测试性、可维护性、可扩展性等方面的缺陷。 经过以上的分析,下面来看一下如何设计。在下图中,每个方框都使用一个函数来实现,为了跟用户界面分开,最顶上的函数不要写在main函数中。 把思路用流程图的方式表达出来,不用停留在脑袋里: 具体的函数的调用关系图: 复杂模块triangleType的流程图: 白盒测试实例之五——编码 1、Triangle.h | /* * Copyright (c) 2008, 胡添发(hutianfa@163.com) * * 三角形类型判断 * */ #include #include /* * 判断一个整数是否在(0, 200)区间内 * 返回值:true-否; false-是 */ bool isOutOfRange(int i); /* * 判断三条边是否合法(即:判断三条边都在合法的范围内) * 返回值:true-是; false-否 */ bool isLegal(int a, int b, int c); /* * 判断两条边之和是否大于第三边 * 返回值:true-是; false-否 */ bool isSumBiger(int a, int b, int c); /* * 判断三条边是否能够组成三角形 * 返回值:true-是; false-否 */ bool isTriangle(int a, int b, int c); /* * 判断两条边是否相等 * 返回值:true-是; false-否 */ bool isEquals(int a, int b); /* * 求三角形有几条边相等 * 返回值:相等边的数量 */ int howManyEquals(int a, int b, int c); /* * 判断是否满足两边平方之和是否等于第三边的平方 * */ bool isPowerSumEquals(int a, int b, int c); /* * 判断第一个数是否比第二个数大 */ bool isGreaterThan(int a, int b); /* * 判断是否是直角三角形 * */ bool isRightRriangle(int a, int b, int c); /* * 判断三角形的类型,返回值: * 1、不能组成三角形 * 2、等边三角形 * 3、等腰三角形 * 4、直角三角形 * 5、一般三角形 * 6、某些边不满足限制 */ int triangleType(int a, int b, int c); |
白盒测试实例之六——单元测试的步骤 白盒测试与黑盒测试的过程和方法是有一些区别的。 单元测试的步骤: 1、 理解需求和设计 理解设计是很重要的,特别是要搞清楚被测试模块在整个软件中所处的位置,这对测试的内容将会有很大的影响。需要记住的一个原则就是:好的设计,各模块只负责完成自己的事情,层次与分工是很明确的。在单元测试的时候,可以不用测试不属于被测试模块所负责的功能,以减少测试用例的冗余,集成测试的时候会有机会测试到的。 举例: | /* * 判断三条边是否能够组成三角形 * 返回值:true-是; false-否 */ bool isTriangle(int a, int b, int c); |
测试该函数的时候,只需要测试三条边(在合法的取值范围内的整数)是否能够满足两边之和是否大于第三边的功能,而不需要测试三条边是否在合法的范围(0, 200)之间的整数,因为调用该函数之前,一定要先通过下面函数的检查,要是检查不通过,就不会执行isTriangle函数。
| /* * 判断三条边是否合法(即:判断三条边都在合法的范围内) * 返回值:true-是; false-否 */ bool isLegal(int a, int b, int c); |
所以,单元测试主要是关注本单元的内部逻辑,而不用关注整个业务的逻辑,因为会有别的模块去完成相关的功能。 白盒测试实例之七——单元测试的尝试 关键字:白盒测试单元测试软件测试 以测试isOutOfRange函数为例,首先知道该函数在整个软件架构中处于最底层(叶子),所以对它进行测试并不需要写桩模块,只需要写驱动模块。要注意的问题是:对于测试结果是否通过测试不要使用printf方式打印被测试函数的返回结果值,否则就需要人工去检查结果了。 使用边界值的方法可以得到5个测试用例,写的驱动模块代码如下: | TestTriangle.cpp: /* * Copyright (c) 2008, 胡添发(hutianfa@163.com) * * 单元测试与集成测试 * */ #include "Triangle.h" /* * 测试isOutOfRange函数,使用边界值的方法(0,1,5,199,200) * */ void testIsOutOfRange_try() { if(isOutOfRange(0) == true) { printf("pass!\n"); } else { printf("fail!\n"); } if(isOutOfRange(1) == false) { printf("pass!\n"); } else { printf("fail!\n"); } } void main() { testIsOutOfRange_try(); } |
小知识:做单元测试的时候,一般不直接在main函数中写所有的测试代码,否则的话,main函数将会非常庞大。正确的做法:针对每个函数分别创建一个或若干个(函数比较复杂时)测试函数,测试函数的名称习惯以test开头。 写到这里发现重复的代码太多了,而且如果测试用例数量很多的话,对于测试结果的检查也将是很大的工作量。在测试有错误的时候,这样的单元测试结果也很难获得更多关于错误的信息。 解决问题的途径可以采用cppUnit单元测试框架。不过这里为了让学生能够对单元测试和单元测试框架有进一步的理解,我决定自己写一个类似cppUnit的简单的测试框架。 白盒测试实例之八——构建自己的单元测试框架(上) 关键字:单元测试、白盒测试 在上一讲“单元测试的尝试”里我们遇到了几个问题: 1、代码重复的问题太多 2、测试结果需要人工去检查 3、对测试的总体信息也无从得知 本讲将构建一个简单的单元测试框架来解决以上的问题: 1、代码重复的问题太多 这个问题很容易解决,只需要把判断预期结果和实际结果的逻辑提取到某个函数中即可。从整个代码来看,有两种类型的结果的函数: (1)返回布尔型 (2)返回整数 因此,需要两个类型的判断预期结果和实际结果是否相符的函数: | /* * 判断是否取值为真 */ void assertTrue(char *msg, bool actual) { if(actual) { printf("."); } else { printf("F"); } } /* * 判断预期结果和实际结果是否相符 */ void assertEquals(char *msg, int expect, int actual) { if(expect == actual) { printf("."); } else { printf("F"); } } |
小知识:XUnit系列的框架的习惯使用assert*的命名来定义判断函数,对于通过的测试习惯打印一个“.”号,而对于失败的测试习惯打印一个“F”。 2、测试结果需要人工去检查 对于测试结果不要使用printf方式打印被测试函数的返回结果值就可以避免这个问题。 3、对测试的总体信息也无从得知 除了问题1的解决办法里使用“.”表示测试通过和“F”表示测试失败可以提高对测试结果的信息的直观性之外,做单元测试的人还希望能够得到以下的信息: (1)执行的测试用例总数、通过的数量和失败的数量 (2)测试执行的时间 (3)如果测试用例执行失败了,希望知道是哪个测试用例失败,从而去分析失败的原因。 白盒测试实例之九——构建自己的单元测试框架(下) 完整的源代码如下: 1、UnitTest.h | /* * Copyright (c) 2008, 胡添发 * * 简单的单元测试框架 * */ #include #include #include #include /* * VC中没有sleep函数,自己写一个 * wait单位是毫秒 */ extern void sleep(clock_t wait); /* * 判断是否取值为真 */ void assertTrue(char *msg, bool actual); /* * 判断预期结果和实际结果是否相符 */ void assertEquals(char *msg, int expect, int actual); /* * 初始化测试,开始计时 */ void init(); /* * 结束测试,结束计时,打印报告 */ void end(); |
白盒测试实例之十——集成测试的概念 测一、桩模块和驱动模块(以C语言为例): 很多人对桩模块和驱动模块的概念会搞不清楚,下面先介绍这两个概念: 模块结构实例图: 假设现在项目组把任务分给了7个人,每个人负责实现一个模块。你负责的是B模块,你很优秀,第一个完成了编码工作,现在需要开展单元测试工作,先分析结构图: 1、由于B模块不是最顶层模块,所以它一定不包含main函数(A模块包含main函数),也就不能独立运行。 2、B模块调用了D模块和E模块,而目前D模块和E模块都还没有开发好,那么想让B模块通过编译器的编译也是不可能的。 那么怎样才能测试B模块呢?需要做: 1、写两个模块Sd和Se分别代替D模块和E模块(函数名、返回值、传递的参数相同),这样B模块就可以通过编译了。Sd模块和Se模块就是桩模块。 2、写一个模块Da用来代替A模块,里面包含main函数,可以在main函数中调用B模块,让B模块运行起来。Da模块就是驱动模块。 知识点: 桩模块的使命除了使得程序能够编译通过之外,还需要模拟返回被代替的模块的各种可能返回值(什么时候返回什么值需要根据测试用例的情况来决定)。 驱动模块的使命就是根据测试用例的设计去调用被测试模块,并且判断被测试模块的返回值是否与测试用例的预期结果相符。 二、集成测试策略: 1、 非增式集成测试 各个单元模块经过单元测试之后,一次性组装成完整的系统。 优点:集成过程很简单。 缺点:出现集成问题时,查找问题比较麻烦,而且测试容易遗漏。 范例: 2、 增式集成测 (1)自顶向下 A、 纵向优先/p> 从最顶层开始测试,需要写桩模块。测试的顺序:从跟节点开始,每次顺着某枝干到该枝干的叶子节点添加一个节点到已测试好的子系统中,接着再加入另一枝干的节点,直到所有节点集成到系统中。 B、 横向优先 跟纵向优先的区别在于:每次并不是顺着枝干走到叶子,而是逐一加入它的直属子节点。 纵向优先的范例: (2)自底向上 每次从叶子节点开始测试,测试过的节点摘掉,然后把树上的叶子节点摘下来加入到已经测试好的子系统之中。优点:不需要写桩模块,但需要写驱动模块。 范例:
|