系统程序员成长计划-写得又快又好的秘诀(五)
2,745 views| 2008-12-09| 李先静| 系统程序员成长计划| | 15 条评论转载时请注明出处和作者联系方式
文章出处:http://www.limodev.cn/blog
作者联系方式:李先静 <xianjimli@gmail.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 多线程运行的程序。多线程的程序也很难自动测试,比如向链表中插入一个元素,当你检查的时候,根本无法知道链表的长度是否增加,也无法知道刚才插入的位置是否是你插入的元素,因为这个时候,可能有另外一个线程已经把它删除了,或者又加入了新的数据。不过在单线程的自动测试通过之后,多线程的问题会大大减少,剩下的问题我们可以通过其它方式加以避免。
写自动测试程序会花你一些时间,但这个投资能带来最大的回报:减少后面调试时的浪费,提高代码的质量,更重要的是你可以安稳的睡个觉了。
本节示例请到这里下载。
系统程序员成长计划 Share
Comments
Tags
Recent Posts
Most Viewed
- 系统程序员成长计划写作提纲 - 19,605 views
- Android IPC机制详解 - 6,277 views
- 系统程序员成长计划-走近专业程序员(上) - 6,253 views
- 系统程序员成长计划-写得又快又好的秘诀(一) - 5,391 views
- 系统程序员成长计划-背景知识 - 5,070 views
- i++循环与i–循环的执行效率 - 4,712 views
- 系统程序员成长计划-Write once, run anywhere(WORA)(上) - 4,700 views
- 系统程序员成长计划-走近专业程序员(下) - 4,254 views
- Linux下的调试工具 - 4,017 views
- Advanced Linux Sound Architecture (ALSA) 研究笔记 - 4,017 views
- 系统程序员成长计划-序 - 3,985 views
- 系统程序员成长计划-写得又快又好的秘诀(三) - 3,930 views
- 中国人与自由软件文化研究(搞笑版) - 3,735 views
- Android中的MessageQueue,Handler,Looper和Thread - 3,686 views
- 答复:我不会OOO,仍然可以XXX - 3,658 views
Categories
- Android (28)
- Broncho-A1-Hack (6)
- DirectFB (7)
- FTK(嵌入式GUI) (24)
- GTK+ (29)
- KVM hack notes (8)
- Linux Mobile (65)
- Management (5)
- Mozilla (9)
- Open Source (5)
- Programming (34)
- Tools (9)
- Uncategorized (23)
- Win32 (3)
- X Windows (31)
- 沉思录 (29)
- 系统程序员成长计划 (67)
Blogroll
gallery
Linux guru
推荐网站
Recent Comments
- Dig on 嵌入式GUI FTK设计与实现-事件源(FtkSource)
- 用心生活每一天 » GNU gprof: linux profiling tools 使用 on gcc profiling的工作原理
- JavaScript for: i++ vs i–-传播、沟通、分享-一直“有你” on i++循环与i–循环的执行效率
- Frankly Law on 嵌入式GUI FTK介绍(11)-交叉编译
- tracing on Linux下的调试工具
- ndljsn on FTK移植指南(初稿)
- tracing on 爬塘朗山
- tracing on GTK+(基于DirectFB)的字体处理
- Kely on 系统程序员成长计划写作提纲
- tracing on 爬塘朗山



December 9th, 2008
Joey.Huang
December 9th, 2008
受教了。
Dig
December 10th, 2008
能看到这篇东西,我实在太幸运了。
yetiboy
December 10th, 2008
我一直以为测试的都没有技术含量的。。。看了文章才知道自己的无知,幸好在别人面前出丑前看到了博主的文章。不过,对于测试,估计在学校里面很少能接触到。我自己只是手动检测一下,从来没有想过测试会是这么一门学问 >’_'<
admin
December 11th, 2008
呵,谢谢几位兄弟长期的支持。
wilwin
December 13th, 2008
已拜读完该篇文章,体会到测试的内涵,谢谢!!继续关注中~~
The linux mobile development » Blog Archive » 系统程序员成长计划写作提纲
February 15th, 2009
[...] 完成 2.1 好与快的关系 2.2 代码阅读法 2.3 避免常见错误 2.4 自动测试 2.5 Save your work 第3章 从动态数组学习设计 完成 3.1 动态数组 3.2 排序算法 3.3 [...]
新风雪雨
February 18th, 2009
很好的一篇文章,继续关注博主的后续文章
admin
February 25th, 2009
谢谢支持
武涛
March 31st, 2009
谨受教
Aaron
April 28th, 2009
李兄,下了你的实例代码参考了一下,发现dlist_insert函数好像有点问题。
if(index next = cursor;
cursor->prev = node;
if(thiz->first == cursor)
{
thiz->first = node;
}
}
没有将cursor原来的前驱和插入的node之间的关系建立起来。
我在李兄的基础上改了一下:
if(index first == cursor)
{ // insert befor first
thiz->first = node;
}
else
{
node->prev= cursor->prev;
cursor->prev->next = node;
}
cursor->prev = node;
node->next = cursor;
}
Aaron
April 28th, 2009
晕,发完发现代码乱的,也不知道是不是因为我用的是日文操作系统的原因。
YHnjupt
March 5th, 2010
代码根本没法下,bbs上也没法注册,希望解决!!!!!!!!!!!!!好事做到底吧
noopy
April 25th, 2010
1. 很喜欢这种通过自身亲身经历,不断演化的方式来说明一个问题解决方法的阐释方式。
2. 用assert方式来检验测试结果的话,如果用例较多的话,会导致不能一次看到所有用例执行情况。这一方面我更倾向于用cunit这样的轻型测试框架来管理测试用例。特别是当这些用例不是由开发人员自己写的时候。
3. 项目如果牵涉的子系统比较多比较复杂的话,要想睡个安稳觉,还需要在自动测试代码之上加一个CI(持续集成)监控,这样时时刻刻都可以自动监测项目的健康状态了。
4. 测试也没有银弹,不同的业务可能需要不同的自动化测试方法和工具,这也是为什么需要专职的测试人员的原因。
Heiher
May 9th, 2010
用基本查询校验复合查询不一定是个好办法,复合查询的实现多数是基本查询的组合吧。
李先静
May 11th, 2010
呵,有更好的办法么?