转载自http://blog.csdn.net/absurd/archive/2008/12/10/3488167.aspx
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli at hotmail dot com>
自动测试
手工测试比没有测试强一点,但是它存在的问题让它很难在实践中应用:手工输入数据的过程单调乏味,很难长期坚持。每次都要重新输入数据,浪费大量时
间。测试用例不能累积,测试往往不完整。用人脑判断输出的正误,浪费人力也存在误差。要写得好测试自然不能省,要写得快就需要更好的测试方法。
更好的测试方法当然是自动测试了。幸运的是,刚进入这个行业我就接触了自动的测试
(呵,读本文的初学者就更幸运了),我的第一份正式工作是在测试组写测试程序。当时测试组也算是人才济济了,居然有几个北大毕业的,不过她们都不懂
Linux,所以我被指派去为移植到Linux上的模块写测试程序。这些模块都有测试程序,但这些测试程序的功能太弱了,我的上司要求开发人员改进,但那
些开发人员太自以为是了,根本不理我们,所以我们只好自己重写这些测试程序。模块很多,大概有50多个模块,熟悉这些模块也需要不少时间,按每两个工作日
写一个测试程序,上司给我5个月时间。
记得第一个模块是RDFParser,RDF(资源描述框架)是XML的一种应用,RDFParser实际上是一个XML解析器,并包装成RDF要
求的接口。由于我对C/C++还不太熟悉,对RDF更不熟悉了,花了两周时间才写出这个测试程序。运行起来有些不正常,我确信不是测试程序的问题,就去请
开发人员帮忙来看一下。负责RDFParser的那个程序员是人大毕业,我没有见过第二个比他更自以为是的程序员了,他刚在我座位上坐下就很大声说,你们
QA的人太蠢了!
当时一听就愣了,不过我是新来的,见上司都没反应,自然就忍了。我列举了一些证据是模块里面的问题,他听也不听,只是不断重复的说,不可能是我程序
的问题,你们QA的人太蠢了,总是浪费我的时间。过了一会儿,他终于闭上了嘴巴,又等了一会儿才说,等会儿重新发个版本给你吧。后来又请他过来四五次,结
果每次都是他的问题。
之后我再没有听到他说过你们QA的人太蠢了的话。为了避免让他抓到把柄来嘲笑测试组,我决定请他来查问题之前做更详细的测试。当时我写的测试程序和
现在初学者写的测试程序没有两样,都是从教科书上学来的,先通过scanf从终端输入数据,调用被测函数,再把结果printf出来,这花了我太多时间。
想到后面还有50多个模块的测试程序要写,这样下去不行,一定得想个办法。
后来我把输入的数据和期望的结果都写到一个INI文件中,测试程序从这个文件中读入数据,运行测试,再和预期结果比较,整个过程都自动化了。写了一
个INI文件的解析器花了我一周时间,又重写了那个测试程序,整整花了我一个月时间完成RDFParser的测试程序。进度自然大落后了,还好上司知道后
并没有责备我,让我慢慢做就好了。
写第二个测试程序时把INI解析的代码拷贝过去,再加一些调用模块的代码就写好了,第三个也是如此。写了几个之后,我发现了INI解析有个BUG,
结果每个测试程序我都要去修改,想到维护起来太麻烦了,就把INI解析器的接口规范化了,编译成一个独立共享库。又写了几个测试程序,我写烦了,原因是测
试程序无非就是读入数据,调用被测函数,再检查结果,这个过程太无聊了。想到后面还要把这个过程重复几十遍,郁闷了几天之后,突然灵机一动,我决定写了一
个代码产生器来产生这些代码。开始的代码产生器用C写的,用一个简单的规则来描述被测函数,通过这些规则来产生测试程序。我把这些东西和INI解析器放在
一个独立的库中,把它叫作TesterFrameWork,经过几个测试程序的验证和完善,后来利用这个TesterFrameWork,只要一两个小时
就能完成一个测试程序了。有次请开发人员那边一个高手帮我查一个问题,他看一会儿我的TesterFrameWork之后,盯着我说,你太聪明了。我笑了
笑说,刚刚开始写C/C++程序。
一年之后我知道了有个CPPUnit之后,为了赶时髦我把TesterFrameWork改名为CxxUnit,非典的时候放假无聊就把它重写了一遍放在cosoft上了(之后没有管过它,或许还在吧)。
一个大系统很难自动测试,而一个独立的模块则是最佳的自动测试单元。自动测试和单元测试几乎成了等价的概念,很多人都以为自动测试就是利用CPPUnit这样的单元测试框架写个测试程序而已,这完全是错误的,就像有人以为有个设计文档的模板,照着填空就能填出好设计一样。
我自己实现过单元测试框架,不是像有些人出于模仿去实现,而完全出于实际的需要,后来我也研究其它测试框架,应该说我对测试程序框架的认识比一般程
序员要深刻。我认为测试程序框架可以减化一些测试程序的工作,但它与自动测试没有密切关系,用不用测试程序框架完全是个人喜好。用测试程序框架未必能写出
好的测试程序,就像用C++未必能写出好的面向对象的程序一样。
虽然我顺利的完成了那个写测试程序的任务,但我一直被一个问题困扰:如何写测试用例,如何去检测结果?这是测试程序框架帮不上忙的。写测试用例还好
说,通过边界值法,等价类法和路径覆盖法找到最常用的测试用例。检测结果呢?有人说很简单啊,判断返回值就好了。那我问一下dlist_insert返回
OK,就真的OK了吗?如果一个函数根本没有返回值,那你怎么判断呢?
测试程序框架是敏捷论者提倡的,在我看来它根本不够敏捷:你要去学习它,了解它的运行机制,要包含它的头文件,链接它的库,有比不用它更敏捷么?重
要的是它根本帮不上什么有用的忙。前面的问题折磨了我一段时间,于是得出一个可能有点偏激的结论:测试程序框架都是愚蠢的,你真正需要的,它根本帮不了你
(我知道这样说会得罪一些用测试程序框架的朋友,如果你想找我讨论的话,请看完本节的附带示例代码再说)。
就在那个时候,我看到了孟岩老师翻译的《契约式设计(Design by Contract)》,读完之后豁然开朗。或许我还没有明白契约式设计的本质,但我确实知道了写自动测试程序的方法,下面我介绍一下:
o 在设计时,每个函数只完成单一的功能。单一功能的函数容易理解,也容易预测其行为。对测试来说,给定一些输入数据,就知道它的输出和影响,这样函数是最容易测试的。
o
在设计时,把函数分为查询和命令两类。查询函数只查询对象的状态,而不改变对象的状态。命令函数则只修改对象的状态,只返回其操作是否成功的标志,而不返
回对象的状态。比如,dlist_length查询双向链表的长度,它不修改双向链表的任何状态。dlist_delete修改对象的状态(删除结点),
并返回其操作是否成功,而不返回当前长度或者删除的结点之类的状态。
o
在设计时,把查询分为基本查询和复合查询两类。基本查询函数只查询单一的状态,而复合查询可以同时查询多个状态。比如,window_get_width
返回窗口的宽度,这是基本查询函数,widget_get_rect返回窗口的左上角坐标,宽度和高度,这是复合查询函数。
o在实现时,检验输入数据,确认使用者正确的调用了函数。契约式设计规定了调用者和实现者双方的责任,调用者需要使用正确的参数,才能保证有正确的
结果。政治家告诉我们,信任但要检查,所以作为实现者就需要检查输入参数是否违背了契约。那怎么检查呢?有人说,如果检查到无效参数就返回一个错误码。这
当然可以,只是不太好,因为大多数人都没有检查返回值的习惯,如果每个地方都检查函数的返回值,也是件很繁琐的事,代码看起来也比较乱。通常我们只检查一
些关键的地方,对于无效参数这样的错误,可能就无声无息的隐藏起来了,这样不好,因为隐藏得越深,发现的时间越晚,修改的代价越大。
在C++和JAVA里,如果参数不正确,通常是throw一个无效参数之类的异常,C语言里面没有异常这个概念,我们需要其它办法才行。有人推荐用
assert来检查,这是一个好办法,assert只在调试版本中有效(没有定义NDEBUG),这样任何无效调用都在调试版本中暴露出来了。如果配合前
面返回错误码的方法,在发布版本中也可能避免程序粗暴的死掉。使用方法如下:
assert(thiz != NULL);
if(thiz == NULL)
{
return DLIST_RET_INVALID_PARAMS;
}
我一直使用这种方法,但是有个问题:无法用自动测试验证assert是否正常的触发了,当用错误的参数测试时,我期望assert被触发,但如果assert被触发了,自动程序测试就死掉了,自动测试程序死掉了,就无法继续验证下一个assert。这是一个悖论!
后来我从glib里面学了一招,它检查时不用assert,只是打印出一个警告,代码也简明了,按它的方式,我们这样检查:
return_val_if_fail(cursor != NULL, DLIST_RET_INVALID_PARAMS);
我们需要定义两个宏,一个用于无返回值的函数,一个用于有返回值的函数:
#define return_if_fail(p) if(!(p)) \
{printf("%s:%d Warning: "#p" failed.\n", \
__func__, __LINE__); return;}
#define return_val_if_fail(p, ret) if(!(p)) \
{printf("%s:%d Warning: "#p" failed.\n",\
__func__, __LINE__); return (ret);}
这样一来,遇到无效参数时,可以看到一个警告信息,同时又不会影响自动测试。
o在测试时,用查询来验证命令。命令一般都有返回值,但只检查返回值是不够的。比如dlist_delete返回OK,它真的OK了吗?我们信任它,但还是要检查。怎么检查?很简单,用查询函数来检查对象的状态是不是预期的。
对于dlist_delete,我们预期:
1.输入无效参数,期望返回DLIST_RET_INVALID_PARAMS。
2.输入正确参数,期望:
函数返回DLIST_RET_OK
双向链表的长度减一。
删除的位置的下一个元素被移到删除的位置。
在测试程序中检查时,因为任何不符合期望的结果都是BUG,所以我们用assert检查。这样有问题马上暴露出来了,定位错误比较容易,通常都不需要调试器。我们这样来检查:
assert(dlist_length(dlist) == (n-i));
assert(dlist_delete(dlist, 0) == DLIST_RET_OK);
assert(dlist_length(dlist) == (n-i-1));
if((i + 1) < n)
{
assert(dlist_get_by_index(dlist, 0, (void**)&data) == DLIST_RET_OK);
assert((int)data == (i+1));
}
(完整的例子请看本节的示例代码)
o在测试时,用基本查询去验证复合查询。基本查询和复合查询返回的应该一致。比如:
Rect rect = {0};
widget_get_rect(widget, &rect);
assert(widget_get_width(widget) == rect.width);
assert(widget_get_height(widget)== rect.height);
o在测试时,预期结果依赖其执行上下文,我们要按逻辑组织测试用例。前面调用的函数可能改变了对象的状态,为了简化测试,在每组测试用例开始时,都重置对象到初始状态。
o 在测试时,第一次只写基本的测试用例,以后逐渐累积,每次发现新的BUG就把相应的测试用例加进去。每次修改了代码就运行一遍自动测试,保证修改没有引起其它副作用。
按着上面的原则,应付正常模块的测试没有问题了,但是下面的情况仍然比较棘手:
o
带有GUI的应用程序。有GUI的程序会给自动的输入数据和检查结果带来困难,有些工具可以部分解决这个问题,特别是针对Win32下的GUI,我很少在
Windows下写程序,所以对这方面了解不多。不过最好的办法还是用MVC模型等分离界面和实现,因为界面通常相对比较简单,可以手工测试,而实现的逻
辑比较复杂,这部分可以自动测试。后面我们会专门讲解分离界面和实现的方法。
o
有随机数据输入。如果有些输入数据是内部随机产生的,那你根本无法预测它的输出结果和影响。比如游戏随机的步骤和无线网络信号的变化。对于我们可以控制的
随机数据,可以提供额外的函数去获取这些数据。对于无法控制的随机输入数据,可以把它们隔离开,在自动测试中,使用固定的数据。
o
多线程运行的程序。多线程的程序也很难自动测试,比如向链表中插入一个元素,当你检查的时候,根本无法知道链表的长度是否增加,也无法知道刚才插入的位置
是否是你插入的元素,因为这个时候,可能有另外一个线程已经把它删除了,或者又加入了新的数据。不过在单线程的自动测试通过之后,多线程的问题会大大减
少,剩下的问题我们可以通过其它方式加以避免。
写自动测试程序会花你一些时间,但这个投资能带来最大的回报:减少后面调试时的浪费,提高代码的质量,更重要的是你可以安稳的睡个觉了。
本节示例请到这里
下载。
分享到:
相关推荐
这些经验中的每一条都是与软件测试有关的一个观点,观点后面是针对运用该测试经验的方法、时机和原因的解释或例子。 本书还提供了有关如何将本书提供的经验有选择性地运用到读者实际项目环境中的建议,在所有关键...
三位作者凭借多年的软件测试经验,汇聚了293条实用的测试建议,旨在指导读者如何更好地进行软件测试工作,包括测试管理、测试策略制定、测试自动化、错误报告等关键问题。本书不仅提供了丰富的实践经验,而且鼓励...
本书《软件测试经验与教训》由三位软件测试领域内的资深专家Cem Kaner、James Bach和Bret Pettichord撰写,旨在分享他们多年积累的软件测试实践和经验,为软件测试人员提供指导和参考。在软件测试领域,理论知识固然...
本书最显著的特点是汇总了293条测试经验建议,这些宝贵的经验不仅来自于作者个人的测试实践,还包括了其他顶尖软件测试专家的智慧。每一项经验后面都附有详细的方法、时机和原因解释,或提供实际的例子来支撑理论的...
7. **《软件测试工程师入门教程》- 软件测试工程师入门教程.rar**:这是一份适合初学者的教程,详细介绍了如何成为一名合格的软件测试工程师,可能包含基础理论、技能要求和实战经验。 这些资源对于学习和深入理解...
总的来说,这个实验提供了实践经验,使学习者能够深入理解自动化测试工具的使用,熟悉测试用例设计原则,以及如何通过性能测试来评估系统的稳定性和性能。通过这样的实践,测试工程师能够更好地诊断和优化软件系统,...
由于提供的部分内容非常有限,这里将基于标题、描述以及常见的软件测试经验来展开详细的知识点。 ### 软件测试工作经验与教训 #### 一、软件测试基础知识回顾 1. **定义**:软件测试是指在软件开发过程中,为了...
本文将深入探讨“软件测试经验与教训合集”中涵盖的关键知识点,帮助新手快速掌握测试基础,并从中吸取专家的经验教训。 1. **软件测试基础**:软件测试是验证和确认软件产品是否符合预期功能的过程。它涉及需求...
在这个“移动APP测试经验分享”文档中,我们将深入探讨移动应用测试的关键方面,帮助你提升测试技能,确保你的应用在发布前达到最佳状态。 1. **功能测试**:这是最基础的测试类型,主要验证APP的所有功能是否按照...
书中以实际的案例为背景,结合实际经验,详细讲述了软件测试自动化的实际设计与实施方法。书中的内容涵盖了从计划、实施到管理的整个自动化测试流程,强调了在自动化测试项目中应考虑的各个方面,例如项目计划、文档...
对于拥有两年以上自动化测试经验的工程师来说,深入理解并熟练应用自动化框架至关重要。本文将详细探讨这一主题,涵盖自动化框架的基础概念、常见类型、设计原则以及如何选择和构建合适的自动化测试框架。 一、自动...
1. **测试覆盖率受限**:手工测试依赖于测试人员的经验和直觉,容易遗漏边缘情况或异常路径,从而影响测试的全面性和准确性。 2. **效率低下**:对于大规模、高复杂度的系统,手工测试耗时且易疲劳,难以达到理想的...
11. **最佳实践与调试技巧**:分享作者的经验,教导读者如何优化测试脚本,定位和修复测试问题,以及使用开发者工具进行辅助调试。 通过阅读《Selenium2自动化测试实战——基于Python语言》,读者不仅可以掌握...
然而,随着经验的增长和专业技能的提升,资深测试工程师,特别是那些擅长自动化测试、性能测试或安全测试的专业人才,薪资水平也能达到甚至超过开发人员。 接下来,我们讨论黑盒测试和白盒测试。黑盒测试,也称为...