这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 综合技术 » [转帖]单元测试简介

共33条 3/4 1 2 3 4 跳转至
菜鸟
2006-12-28 02:15:00     打赏
21楼
6.边界测试

边界测试基础

边界测试就是依据各数据类型预先定义的边界值,自动生成测试用例进行测试。VU把边界测试作为完整测试的第三阶段,即在完成基本功能测试、白盒测试后,还要运行边界测试


达到100%的语句、条件、分支、路径覆盖后,可以说测试完整性已经非常高了,为什么还要运行边界测试呢?因为有一种常见的代码错误是白盒覆盖不能发现
的:忘记考虑特殊输入!由于白盒覆盖是以代码为基础的,如果处理特殊输入的代码不存在,白盒覆盖当然不会发现"某某代码未覆盖"了



特殊输入一般跟数据类型有关,并且未处理特殊输入常常导致程序产生异常,根据这两个条件,预先为数据类型定义特殊的值(边界值),依据这些边界值自动生成测试用例进行测试,常常能够发现意料之外的错误,这就是边界测试




边界测试很简单,只要打开"边界测试开关",运行测试时就会同时执行边界测试




假如这里没有考虑输入可能是空指针




测试时也没有考虑到空指针输入




已经完成白盒覆盖了,但是并没有发现忘记判断空指针的问题。




现在运行一下边界测试




打开边界测试开关就可以运行边界测试。




这是边界测试开关




再次运行测试后,VU报告产生了一个异常,点击它看一下是什么原因




当输入为空指针时,产生了异常。发现了问题,解决起来就简单了:在代码中加入处理空指针输入的代码,重新完成白盒覆盖




有些程序错误并不会产生异常,可以浏览输入输出数据来查看是否有错误。看一下这个最简单的示例




打开边界测试开关后,运行测试




有26个测试用例。VU会把人工建立的最后一个测试用例设为当前用例,因此可以看出,人工建立的测试用例只有1个,其他25个都是自动生成的边界测试用例




右击打开快捷菜单




由于VU把人工建立的最后一个测试用例设为当前用例,因此,下一个就是边界测试用例了,便以浏览边界测试的输入输出




两个负数相加,结果等于0

两个负数相加,结果等于一个很大的正数

到这里,应该可以发现问题了:当输入很大或很小时,会产生溢出错误




各种数据类型都可以定义边界值,如果参数的数据类型未定义边界值,打开边界测试开关后,编译时会产生错误。




边界测试也可以断言预期输出,及过滤部分输入,后续章节会进一步讨论


菜鸟
2006-12-28 02:15:00     打赏
22楼
定义边界值

各种数据类型都可以定义边界值。系统已为基本数据类型定义了边界值,可视需要增删或修改

如果函数的参数类型未定义边界值,打开边界测试开关后,会产生编译错误




点击这里打开"定义边界值"窗口




输入或选择类名




用typedef定义的类的别名,或用#define定义的类的别名,填在这里




这里填写边界值,每行一个值,不能加任何无关符号或注释

数据类型分为三种,分别使用不同的语法:

基本数据类型,直接填写边界值;

结构,用.操作符给每个域赋值;

类,调用构造函数赋值,定义了构造函数的结构也推荐使用这种语法




看一下已定义的边界值




这是一个别名,多个别名用","分隔




可以随时增加或删除某些值




点击这里保存




由于宏语法的局限性,两个或多个单词的数据类型要通过别名来定义,这是为unsigned
int类型定义的边界值。系统已为这类数据类型定义了边界值




为结构定义边界值的语法是:用.操作符给各个域赋值,多个域用","分隔,一个对象的所有域的值只能占用一行,不能换行




可以使用多级.操作符。现在为结构RECT定义边界值,它有两个POINT类型的成员:leftTop、rightBottom




这样比较麻烦,比较复杂的结构最好定义一个构造函数,调用构造函数来定义边界值




类或定义了构造函数的结构,直接调用构造函数赋值




第一个是缺省构造函数




假如CMyClass类还有一个构造函数:

CMyClass(const char*, int);

那么还可以添加这样一个边界值...



菜鸟
2006-12-28 02:15:00     打赏
23楼
边界测试的输入输出

边界测试的测试用例是依据参数类型自动生成的,数量往往很多,例如,如果一个函数有两个参数,每个参数有五个边界输入,那么两两组合就形成了25个测试用例。分别为那么多的测试用例断言预期输出是没有意义的

一段程序所涉及的各个数据之间,有时会存在一些固定的关系,例如,三角形的三条边具有任意两边之和大于第三边的关系,如果这种关系被破坏,那么程序必定存在问题,我们可以根据这些不变关系来为边界测试用例定义预期输出




在测试用例编辑器中,最后一个测试用例就是边界测试用例(严格来说,是一组测试用例),我们也可以定义输入和输出,与普通用例不同的是,这里定义的输入与输出是针对于所有边界测试用例的




由于参数i和j及返回值ret具有这种关系:

ret = i+j,所以,

如果i>0,那么ret>=j;

如果j>0,那么ret>=i;

我们可以输入这样的预期输出...




这两个表达式描述三个数之间的以下关系:

如果i>0,那么ret>=j;

如果j>0,那么ret>=i;




运行测试后,会出来一堆错误




一个函数所涉及的数据一般有参数、返回值和部分成员变量,如果能找到这些数据之中的任意两个或多个之间的固定关系,就可以作为边界测试的预期输出。当然,如果没有这种关系,也不必勉强,通过浏览边界测试的输入输出数据来查找程序错误也是很方便的途径




在函数的入口处添加一些断言对输入进行检查是很常见的。有些边界值可能不符合这些断言,在这种情况下,要过滤掉不符合入口断言的输入




过滤输入的方法很简单:把入口断言拷贝到边界测试用例的输入中,把ASSERT改为TEST_FILTER就行了




现在试一下不过滤输入就运行边界测试




运行测试边界时,产生断言错误,因为有些边界输入不满足入口断言。这时最好终止测试




把代码的入口断言粘贴在这里,并把ASSERT改为TEST_FILTER就行了




运行测试,不会再产生断言错误,测试用例的数量也减少了




涉及到成员变量的数据过滤的语法稍有不同




成员变量前面要加上pObj指针




这里运行测试后的结果



菜鸟
2006-12-28 02:15:00     打赏
24楼
7.测试过程实例

完整的测试过程





用这个函数演示一个函数的完整测试过程,代码的功能很简单:计算g的e次幂,g和e都是无符号整数。代码的逻辑也很简单

在函数列表中选中它


自动弹出"生成测试函数"窗口,点击"确定"即可生成测试函数


根据程序的功能,很容易就能想到几个测试用例,这时候不必关心是否有遗漏,想到几种输入就建立几个测试用例。只要基本了解程序的功能,一个或几个典型的测试用例通常是"现成"的


点击这里以拷贝/修改的方式建立下一个


假设就想到了这几种输入,点击"确定"保存测试用例


编译并运行测试工程,即可执行测试。后文所述的"运行测试"或"重新运行测试",都是指编译并运行测试工程,不再重复


自动弹出主窗口,显示测试结果。


可以看到,有一个失败的测试


单击失败的测试,所有窗口都会切换到相关的数据


预期的返回值是3125


实际的返回值是0


输入数据,两个参数都是5


看一下这个测试用例执行了哪些代码


很容易看出,由于前面把result初始化为0,所以这里循环多少次,结果都是0


还有一种查看当前用例所执行的代码的更快捷方式:将鼠标移到路径入口,会显示当前路径所执行的代码,也就是当前用例所执行的代码



完成了测试的第一阶段:基本功能测试。由于不需要编写测试代码,同时典型的测试用例通常是很容易想到的,所以这一阶段所花费的时间一般很少。接下来是第二
阶段:完成白盒覆盖。如果边开发边测试,在完成第一阶段后,应考虑代码是否需要优化,例如这个示例的代码就可以写得简洁些。最好先完成代码优化,并重新通
过测试后再完成白盒覆盖


这是未覆盖的代码,带背景的代码都是未覆盖的


还有一些分支未覆盖,分支标注的背景色是蓝色的分支都是未覆盖的


路径方面,还有两条路径未覆盖


白盒覆盖的工作顺序是语句、条件、分支、路径。


单击选中一个未覆盖语句,右击打开快捷菜单


打开测试用例设计器


提示很清楚,把e改为0就OK了


点击这里保存新的用例


重新运行测试后,可以看到,已经覆盖了所有代码


看看逻辑结构图,只有一个分支未覆盖了,这是一个空分支,表示不进入循环的情形


这里的提示是:在符合前面的条件的前提下,不进入这个循环


从这两个条件可以看出,e是大于1的数,所以,不进入下面的循环是不可能的


条件无法满足,可以肯定,要覆盖的分支是不可覆盖的,不可覆盖的分支应在逻辑结构图中删除


把不可覆盖的分支删除后,所有经过该分支的路径也会自动删除。可以看到,已经完成了100%的语句、条件、分支、路径覆盖了



此,完成了测试的第二阶段。第一阶段的基本功能测试和第二阶段的白盒覆盖是互相依存互相补充的:由于有第二阶段来保证测试的完整性,所以第一阶段只需要建
立典型的容易想到的测试用例,不必费时费力地设计完整的测试用例集;第二阶段是在第一阶段的基础上,统计白盒覆盖率,并在现有测试用例中计算近似测试用例
和修改条件,以便快速地设计出剩余的测试用例,轻松地达到空前的测试完整性


即使达到了100%的语句、条件、分支、路径覆盖,程序仍然可能隐含错误,例如,如果有些特殊输入没有相应的处理代码,那么白盒覆盖并不能发现,而边界测试正是为找出这种错误而设计的,这是测试的第三阶段


打开边界测试开关


再次运行测试


测试的运行时间远远超出了预期,可能有死循环或很耗时的代码,点击这里结束测试


可以通过浏览边界测试的输入输出来查找程序错误。右击打开快捷菜单


VU会把人工建立的最后一个测试用例设为当前用例,因此下一个就是边界测试用例了


注意参数和返回值的变化



面一直都没错,到这里可以看到问题了。当g和e都很大时,超出了计算机的能力,同时也可以想到,当输入较大时,返回值也会溢出,所以,这个程序要考虑如何
处理大的输入,实际上,现在这个函数只能处理很小的输入。当然,测试只是发现问题,如何修改设计就不是这里应有的话题了


边界测试出现死循环或崩溃时,一般是已执行的最后一个测试用例的问题,在这里直接填写它的编号,就会切换到最后一个测试用例


简单总结一下,三阶段完成彻底的测试:

*
根据程序的典型输入建立一个或几个测试用例,这种测试用例是很容易想到的;

*
针对未覆盖的逻辑单位,使用测试用例设计器设计覆盖用例,达到100%语句、条件、分支、路径覆盖;

* 运行自动边界测试检查预料之外的隐含错误



2006-12-23 22:55:41---------------------------------------------------------------

菜鸟
2006-12-28 02:16:00     打赏
25楼
复杂的输入输出

一个函数的输入输出可能很复杂,例如:参数是复杂的结构或对象、需要读写一些成员变量等等。我们用这个示例程序来演示当输入输出比较复杂时的测试

由于要找出一段既容易理解,输入输出又很复杂的程序比较困难,所以使用这样一个没有实际意义的示例代码




参数中,PERSON结构的定义如下:




struct PERSON

{

string name; //姓名

UINT sex; //性别

UINT age; //年龄

string title;//称呼

COMMUNICATION comm; //通讯方式,本身也是一个结构

};




这个参数表示"成长的年数"




即使被测试对象本身有很多成员变量,但对于一个函数来说,即使代码很复杂,它所需要读写的成员变量通常也不会很多。另一方面,即使参数或成员变量是很复杂的结构或对象,但对于一个函数来说,它所需要读写的域或成员也不会很多。




测试时只需要为函数所读取的数据设定初始值,及只需要为函数所写入的数据判断输出,不必理会其他无关的数据,这是输入输出比较复杂时的测试要领




成员变量中,需要读取和写入的有:mAge




pPerson中需要写入的域有:age




pPerson中需要读取的域有:sex




pPerson中另一个需要写入的域:title




被测试函数只读取了pPerson的一个域:sex




被测试函数只读取了被测试对象的一个成员变量:mAge




完成了输入的设置。在实际工作中,不一定每个涉及的输入都要分别设定初始值,例如,被测试对象一些成员变量在构造函数中已初始化,如果不需修改,则不必再次设置初始值。




设置初始值的方法也很灵活,可以调用被测试对象成员函数来设置某些成员变量的初始值,可以调用参数对象或成员变量对象的操作来设置初始值




有返回值的函数,返回值是一个首要的输出




被测试函数只修改了一个成员变量:mAge




被测试函数改写了pPerson的age域




被测试函数还改写了pPerson的title域




这里把被测试函数所改写过的所有数据都列出来了,在实际工作中,可以根据函数的设计功能,只列出其中的一部分




可以把输入输出排列整齐




前面说过输入输出比较复杂时的第一个要领:输入只考虑需要读取的数据,输出只考虑需要写入的数据。




第二个要领是建立了第一个测试用例后,编译测试工程,没有编译错误后再建立其他测试用例。这是因为输入输出比较复杂时,容易因为录入问题造成编译错误




没有编译错误,现在建立其他测试用例




一般来说,只需要修改个别数据




采用这个测试用例,需要修改的数据较多,可以另选一个。这是输入输出比较复杂时的第三个要领:新建用例时选择拷贝所需修改较少的用例




这是测试结果




总结一下,输入输出比较复杂时的测试要领有三个:

*
输入只考虑需要读取的数据,输出只考虑需要写入的数据;

*
建立了第一个测试用例后,编译测试工程,没有编译错误后再建立其他测试用例

* 新建用例时选择拷贝所需修改较少的用例


菜鸟
2006-12-28 02:21:00     打赏
26楼
复杂的代码逻辑之一

演示比较复杂的代码的测试。这个函数的功能是删除C++代码中的注释。为了使这个示例不致于太过复杂,不考虑注释开始符或结束符位于字符串内的情形,即假设输入是不含字符串的代码。如果读者要使用这个函数删除代码注释,要先过滤代码中的字符串,勿谓言之不预也
~-~

正在读取单行注释




正在读取多行注释




读取代码字符,当cmmSin为真或cmmMul为真时则跳过,否则将字符保存到pDes




参数pSrc为源代码




参数pDes保存删除注释后的代码




当cmmSin和cmmMul均为假时,检查读到的字符是否是注释开始符




正在读取单行注释时,以换行符为注释结束符




正在读取多行注释时,以*/为注释结束符




完整的代码请阅读CMyClass::DeleteComment()源代码。由于测试不熟悉的代码比测试熟悉的代码要困难得多,白盒测试方面更是如此,如果不了解代码,可能会觉得难度较大,如果观看本节和下节时觉得难于理解,请先阅读被测试的源代码




这是输出参数,要申请足够的内存




char*类型未定义==操作符,要使用字符串比较函数




第一个测试用例建立完成




加一行




多行注释




注释位于一行的中间




空字符串




可以把用例分类,把空串输入归为边界输入这一类中




基本功能测试用例建立完毕,你觉得有遗漏的吗?




执行测试,没有发现代码错误




有一个语句块未覆盖




cmmSin和cmmMul均为假,即不是正在读取注释




当前的字符是"/"




下一个字符是"0",即字符串结束。也就是说,输入的字符串在一个"/"字符后即结束




改为这样




运行测试后,这行代码覆盖了




发现了一个错误




当最后一个字符为"/"时,结果把它丢了,显然这是不正确的。出错的具体位置在哪里,本节就不作讲解了




未完成分支覆盖和路径覆盖,下节继续


菜鸟
2006-12-28 02:21:00     打赏
27楼
复杂的代码逻辑之二

接上节。已完成100%语句和条件覆盖,但未完成分支和路径覆盖。来看逻辑结构图

800*600分辩率,不能完全显示逻辑结构图。调整显示比例




试一下覆盖这个分支




len != 0,即输入不是空串




既然不是空串,那么不进入循环是不可能的。该分支不可达




cmmSin != false 或 cmmMul != false,而

cmmSin == false,那么,

cmmMul 一定不等于 false,所以,

cmmMul == false 是不可能的,该分支不可达




cmmMul != false,即正在读取多行注释




读到了一个"*",但紧跟"*"的字符不是"/",也就是说,多行注释在结束符之前含有"*"




运行测试后,这个分支覆盖了




不是正在读取注释




读到"/"之后




下一个不是字符串的结束符




也不是"*",即连起来不是/*




也不是"/",即连起来也不是//。明白了吗?就是一个斜杠,例如除号什么的




运行测试后,这个分支覆盖了




同时把这个分支也覆盖了




已完成全部分支覆盖,只剩一条路径了




增加了一个失败的断言,等一下再看




看一下最后一条路径能不能覆盖




看一下最后一条路径能不能覆盖




所以,这里cmmMul和cmmSin都等于false是不可能的,该路径不能覆盖




完成了全部白盒覆盖




完成了全部白盒覆盖




实际输出似乎没有问题




再看预期输出,忘了加上"int j=2/3; "。别忘了,当出现失败的测试时,也可能是预期输出弄错了




现在来看一下使用白盒覆盖手段所增加的几个测试用例




非注释中有"/"




最后一个字符是"/",这个用例发现了代码错误




多行注释中有"*"




这几个用例都是很容易被忽略的,其中一个还发现了错误。白盒覆盖并不是目的,真正的目的是找出遗漏的测试用例。白盒手段用于保证测试的完整性,是相当可靠的




最后打开边界测试开关,运行一下边界测试




运行测试后,报告了两个异常




两个参数都没有考虑空指针的情形



菜鸟
2006-12-28 02:22:00     打赏
28楼
快速排错
由于测试结果提供了丰富的信息,当产生了失败的测试时,多数情况下都不需要单步调试,通过对比输入输出、浏览用例所执行的代码一般都能快速地找出错误原因

点击失败的测试,产生失败测试的用例会成为当前用例,所有窗口都会显示相关的数据




预期输出是返回值为3125




实际输出是返回值为0




两个参数都是5




可以在代码窗口浏览当前用例执行了哪些代码,黑色的代码就是当前用例执行了的代码




不过更快捷的方式是把鼠标移到这里稍作停留,会显示当前路径所执行的代码,也就是当前用例所执行的代码




只显示当前用例执行了的代码(黑色代码),及计算过的判定,红色表示计算结果为假,绿色表示计算结果为真,程序的行为一目了然




由于这里把result初始化为0




所以这里循环多少次,结果都是0




再来看这个例子,有三个失败的测试。多个错误往往出自同一个原因,所以一般来说,排除一个错误要重新运行测试,然后再对付下一个




预期输出是"abcde"




实际输出多了一个'0',如果结果正确,这个'0'的位置应该是一个字符串的结束符'0'




这里多了一串尾巴,也是因为'0'成了'0'




预期输出是空串,实际输出是一个'0'




对比预期输出与实际输出,不难想到:错误的原因可能是某些代码在设置字符串的结束符时,把'0'写成了'0'




来看一下代码,首先找一下有没有把'0'写成'0'地方




没找到




代码太多,一屏显示不完时,双击窗口上半部分可以向下滚屏,双击窗口下部分可以向上滚屏




这里把'0'写成了'0'




再来看这个示例,有两个失败的测试




这个在上一节讲过了,是预期输出的错误




预期输出是"int i=0;/"




实际输出少了最后的'/'




看一下代码




这里,在读到一个'/'后,如果字符串结果,就直接跳出循环,所以把最后的'/'丢掉了。break;前面加上*pDes++
= ch;应该就对了


菜鸟
2006-12-28 02:22:00     打赏
29楼
调试

需要单步调试时,VU提供的调试增强功能可以提高调试效率

VU的调试增强功能由VU生成的测试代码提供,并且涉及到的调试器命令是一般调试器都具有的,所以可以和各种C/C++调试器兼容。这里以VC6.0为例




VU的调试增强功能有:

自动选择输入数据;

自动添加断点;

无限制后退;

切换输入数据;

增强编辑继续功能




涉及到的调试器命令有:

步进(Step Into)

步过(Step Over)

执行到光标所在行(Run to Cursor)




点击失败的测试,将出错的用例设为当前用例




点击这里打开调试开关,VU将处于调试工作状态,要进行测试时,需关闭调试开关




以调试方式运行测试工程,注意是测试工程不是产品工程




这行代码的功能是自动添加断点,程序会在这里自动中断,执行"步入(Step
Into)"两到三次就会进入被调试函数




步入




已进入被测试函数




来看一下输入数据




这是出错的用例的输入数据。这里会自动选择当前用例的输入作为调试输入




可以后退,假如执行到这里,又想回到开头




点击开头这一行




点击 Run to Cursor




就能实现后退。这种后退与修改EIP寄存器实现的后退是不同的,因为这里把相关变量的值也"退"了回去。实现的原理是重新执行该测试用例的代码,这样,被测试对象、参数都会重新设置值




现在到了函数的出口




如果没有完成调试,可以再回到开头,后退的次数不限




再一个功能是切换输入数据。有时候可能需要跟踪一下其他输入时程序的行为来进行对比,只要改变VU的当前用例就行了




现在想使用这种输入进行调试




回到调试器,在程序的开头点击




执行Run to Cursor




输入数据已经换了,来看一下




再切换回出错用例




再次执行Run to Cursor后,输入已切换为g=5,e=5




调试器的编辑继续功能很不错,但有时候,有些数据的值没有做到自动修改,尤其是修改的位置是执行过了头的代码时。VU的调试增强功能克服了这个缺点




无论编辑时已执行到哪一行,修改后用 Run to Cursor
回到开头,编辑后的代码就会完全生效了




这个返回值是对的了




结束调试




再次运行测试前,要关闭调试开关,否则测试不会运行




这是测试结果


菜鸟
2006-12-28 02:22:00     打赏
30楼
9.开发过程实例

边开发边测试之一

边开发边使用VU进行单元测试,VU所提供的测试数据,有助于整理、验证编程思路,有助于随时修正编码过程中的思路错误、录入错误,提高编程生产率,也会在一定程度上增加编程工作的乐趣

我们用这个示例:删除字符串两边空格的函数,来做边开发边测试的演示。现在写一个同样功能的函数strtrm2()




只在头文件中编写了函数声明,未在源文件中编写实现时,函数不会出现在函数列表中




在这里编写函数实现




有返回值的函数,最好先加一个返回值,能够通过编译后再生成测试函数




试编译一下




编译通过,现在可以生成测试函数了。VU主张在这个时候,即真正开始编写实现代码的时候才生成测试函数,因为这时函数名、参数表、返回类型等应该确定下来了,由于测试函数高度依赖于产品函数,这样可以避免过多修改测试代码




这时建立的测试用例主要用于支持编码,可以把想到的测试用例一次建立,也可以只建立一个最典型的测试用例




我们只建立一个测试用例,其他用例在完成编码后再建立




试一下运行测试




当然,测试是不可能通过的,现在开始编码




编写代码时,建议在测试工程中进行,随时编译运行测试工程进行测试,不必在两个工程间频繁切换




空指针不作处理




空字符串也不作处理




很多程序员都习惯在写了一小段代码后,停一停,执行程序并单步跟踪,或用MessageBox、Trace之类的功能输出一些变量的值...




在VU的支持下编程,一般不需要单步跟踪,各种对象、表达式的中间结果可以用TEST_TRACE宏直接输出




TEST_TRACE宏只在测试工程中被编译,不会对产品工程的编译结果产生影响




直接编译测试工程、执行测试




这里可以看到,右边的空格被删除了,结果没错




来看一下输出各种对象或表达式的中间结果的语法




对象、指针、引用均直接使用TEST_TRACE(),不需要进行格式化




高级数据类型也一样。如果数据类型未添加测试支持代码,则会输出:Undifined




还可以直接输出this指针




表达式也没问题




任何数据类型的对象、指针、引用、指针的指针、表达式都使用同一语法,够方便吧?




与其他输入输出数据一样,中间结果也是针对一个用例的,可以切换用例浏览




中间结果与其他输入输出数据对照,有助于了解程序行为,整理编程思路,提高编程效率



共33条 3/4 1 2 3 4 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]