技术开发 频道

如何才能保证测试代码的正确性

  写测试是一个很奇怪的过程,大脑的思维方式和直接写应用代码好像有些不同,呵呵,没搞清楚,我想这是咱和大师的区别吧,还只是只可意会不可言传,我一直的观点是,真正懂的人是能将模糊的东西解释成清晰的概念的。无论如何在我准备开始动手写这痛苦的测试代码时,我又有新想法了,为什么不在开始就不获取冗余数据呢,虽然Decorator模式在这里的应用很具有吸引力,因为它本身毕竟够简单明了,不过,同时想到Document对象本身的一些特点,首先它是大对象(呵呵,感觉上DOM的对象都好大哦,但其实还没有真正理解它到底是怎么个大法),其次,如果是过滤数据,那么势必要遍历该Document的节点,这样的话似乎处理起来的效率不怎么样,这怎么都不如在一开始就不从ResultSet中获取冗余数据来得简单明了。于是想到,在自动生成column节点上现在还是很弱的,可以用Builder模式来重构它,这样的话,可以通过替换column节点的builder来实现过滤,而且目前来说,也确实是只有自动生成column节点的情况才需要过滤冗余数据,所以也不需要考虑由XML配置模板定义好columns节点的状况;而原来默认的生成方式可以立刻就被提炼出来作为默认builder;这样添加的代码实际也很少,我想几乎更少些吧,因为原来在处理冗余数据时需要遍历Document,而现在只要认准了从ResultSetMetaData获取的字段名,对需要过滤的字段不生成column节点就Ok,而有新的变化出现时,只要替换重新实现builder接口就可以了。当然了,原来这个ResultSet解析器是有测试类的,也有相关的测试,那么将原来生成column部分代码提炼出来作为默认的builder后,可以跑以前的测试,啊哈哈哈哈,这几乎不费吹灰之力,ECLIPSE的重构功能很容易将生成column的代码提炼成一个方法,然后把这个方法提升成一个接口,接着如此如此(不用说了)就弄好了一个默认builder实现,然后解析器加个builder类属性,同时生成它的set方法,一切ko后跑测试(这里都是工具自动做的多,我这一步跨的是会大些的,不过多跑测试总是没什么坏处的,反正跑一次也就几秒中,也许还不用呢,^_^),一路绿灯。重构完成,sweat(注意到了吧,到目前为之,其实我没有写测试,而是使用了原来的测试代码,而代码部分也多是自动生成的。最重要的是,这里使用builder模式,其接口也被测试过了,所以后面加的builder实现就可以不考虑这点了,而只是完全的功能需求测试,功能需求总是可以很单一的,自然就简化了测试代码)。

  现在又回到开始了,我的目标又换了,呵呵,新目标:一个builder接口实现,过滤冗余的字段。如何测试呢?一个ResultSet做输入参数是必要的,输出的话要没有冗余数据,实际应用中要过滤的字段名是固定的(呵呵,别忘了我的最初的目的,是过滤由于SQL查询使用公共接口导致有相同的冗余字段出现。),我可以SELECT出来一个空结果集,其字段都是我要过滤的那些字段名(这里,如前所述,数据体不是我要关心的内容,因为数据体的是根据column节点生成的,所以我只要检查column节点对不对就可以了,这样也简化了我的测试代码),当然,在这些之前是重置ResultSet解析器的builder对象为将要实现的目标代码类,很快就完成了所有的代码,跑测试,出错,修改,跑测试,Kent Back的测试模式显得那么简单,最后,终于一路绿灯了。呼………..

  呵呵,当然不忘了最后一步,Clean code that works。我对目前这个状态下去提炼原代码,有个比喻:关起门来打狗。这时的重构真可以说瓮中捉鳖十拿九稳,最不济也只是回到原地,不过通常的结果总是出人意料的要。不管如何,开始重构,几个关键重构:首先默认的builder可以用一个Singleton模式,当然了,实现过滤冗余字段的builder也是,因为这个生成column节点的代码是不受多线程影响的;然后过滤冗余字段的builder实际可以是一个默认builder的Decorator,三下五除二搞定。最后测试通过,CHECK IN代码。

  这是我的一次经历,可以看到,早先的测试代码也起了作用,所以,只要代码还要跑,那么测试代码永远也不会多余。而要保持清晰的测试代码,不仅需要清晰需求分析、设计以及良好的原有目标代码,一点点偷懒的精神,也是必要的,^_^。而在需求分析和设计这里,测试优先有一种不可抗拒的力量让你深入需求,特别是让你作为你的目标代码的用户来考虑这些代码的使用。本来这个例子只想提下,没想到写出来这么多,sweat。


  OK,下面开始逐一讨论:

  一、TEST FIRST

  TEST FIRST其核心不在“TEST”,而是“FIRST”。测试驱动开发的目的是开发,而测试优先是其过程,从测试开始,以测试结束,中间是小增量的迭代开发,测试代码和开发代码同步进行(注:其实我认为测试代码也是开发代码,不过为了区分这2者,下面所说到的开发代码都指不包括测试代码部分;另外,这句话本身要是仔细扣是有问题的,并不全面,只是测试驱动开发的一个方面来说)。

  不是说要保证测试代码的正确性就要TEST FIRST,而是TEST FIRST可以最大限度地保证测试代码的正确性。(这里要说回代码本身,无论是测试代码还是开发代码,无非是一种开发语言的代码,要保证代码的正确性,我认为主要是良好的设计、清晰的结构以及语言的合理使用,其中保持代码结构清晰是最容易做到的,简单的缩进,统一的编码风格基本就算合格了。对于测试代码,也是一样的道理。)如果不先写测试,那么一定是有目标代码了(废话好像,^_^),这在写测试代码时必然会受到限制,别说自己可以不理会写好的目标代码(我想90%的人还是比较难做到的吧,都写好了,还改来改去,而且一个接口一旦公布出去,由不得你想改就改),写测试的时候,束手束脚,这不能动,那不能改,你怎么办,当然是按照自己写开发代码的思路来写测试了(呵呵,好像如果是先写了开发代码而后写测试代码,测试代码的目的也多是为了验证开发代码的正确性而已,至少以前我有这样做时是这样的状况),于是你的思维被禁锢了,测试代码的好坏(这里的坏不是说不能用,^_^),就由开发代码控制了。说回到我举的例子,如果开始的时候确实是因为时间关系、以及需求的突发性改变导致没有很细致地考虑如何修改设计(实际上我认为这样的状况实在太多,我接触的用户,有很确定地提出某个需求,而最后由于来看演示的用户完全否决该需求而导致完全推翻重来的,不过要说的是,我面对的用户实在没法子,人改需求的用户比当初提需求的用户官大,sweat,不改不行,时间还不能拖,原因就不细说了),直接就由开始的构思来写好目标代码先,那么会是什么样的局面?首先是时间已经花进去了(呵呵,单就这个例子当然其实也没多少时间的,是相对来说的),而到开发代码完成要写测试的时候必然导致写测试的目的是验证这段代码的正确与否,这样的话,不用说,准备好输入数据,验证输出数据嘛,哪还管那么多?(我认为是这样的,一旦确定了目标,而且还是那么实在的目标,做起事来的时候,脑袋里早就计划好了,就没有余地来考虑其它东西了,更不要说跨出这件事情考虑做另一件事情了)。而采用测试优先的方式的话,其结果和过程我在例子里面也写的很详细了,没有目标代码写测试其实是很轻松的事情,因为现在什么都还没有,自己想怎么就怎么样,想让测试代码清晰、简单明了,那还能复杂到那里去?

  所以说,TEST FIRST是提高测试代码正确性的一个很好的途径。这里先写开发代码后写测试代码的方式,其实说白了,是我想说的第三点,那样的测试,真的就很容易成了为了测试而测试的代码(记忆里,以前我这么做的时候,还都是为了测试而测试,呵呵,不知道其他朋友会如何)。

  二、只写出需求测试(注意,是目标代码需求,这个代码的用户当然是自己了,^_^)

  前面说到,要保证测试代码的正确性,从保证代码本身的清晰来入手,只写需求测试也是一个道理,不过要把它放在这里,主要也还是因为自己以前写测试的时候,往往都很容易忘记这一点,简单的比方说,当一个测试会牵涉多个类的时候,本来其实也很明确的,单元测试嘛,你测试你的目标类就OK了呗,但是每个人的认识不同,写测试的水平不同,更在于小心谨慎,总是在写目标类测试的时候,想这个地方虽然是A类做的事情,不过我也连带着加个断言看下A做对了没有吧,反正也就多加一句,以防万一嘛(太小心了,其实过界了都不知道)。别小看一行代码,我的感觉,超过20行(空行不算,sweat)的一个方法在你写完这个方法几个月后回来看,晕的几率很大,至少要立刻明白这个方法的内部结构是基本不能。积少成多,如果目标类关联了A、B、C、D、E五个甚至更多,多出来的恐怕就不是一、二行代码了,而且你一次小心一定会多次小心的,比如,在一个测试用例中(我所指的测试用例不是指TESTCASE类,而是该类中的一个TEST方法)你的断言中包含了对A的一个操作结果的测试,那么在另一个测试用例中,如果还用到A的这个操作,我肯定你还会写这个断言,因为你第一次的小心不是偶然。人说,小心使得万年船,不过,杞人忧天就过犹不及了。而写测试的时候,是很容易就会犯这样的错(唉,其实是我当初很容易犯这样的错,sweat)。

  所以,职责分明,只写你需要的你该做的测试,别一开始就怀疑这个怀疑那个,处处小心,步步为营。我们要如何使用我们的代码,那么就写那样的测试,现在我都是拿测试代码当例子,写了一个公共模块,除了有文档以外,都是建议要是觉得看文档麻烦就看测试代码如何使用,或者你干脆就拷贝我的测试代码过去改下用好了。

  三、不要为了测试而测试(这句话是和一个朋友聊天时他说是他和Kent Back交流时Kent Back提醒他的,这里我也不是很确定对这句话的理解的正确与否,因为理解一句话,上下文也是关键的,而我并不很了解我朋友同Kent Back谈话的具体内容和过程,不过这里还是作为一个要点谈谈自己的想法)

  不要为了测试而测试,^_^,虽然说要写测试,不过大家都是开发的,如果你的测试代码的目的成了测试,我还是会说你这就不够“专业”了。(这句有点耳熟,星星:拜托,虽然大家都是中国人,你要是抄我台词,照样要告你剽窃)

  其实开始我听朋友讲这句,似懂非懂,因为毕竟只是一句没头没尾的话,听过就算了。但是细细品了,确觉得有些道理(^_^,不知道我的想法是不是Kent Back说这句话的本意)。言归正传,开发人员的测试代码,还是不要以测试目标代码为目标的好,这样才能更好的确保测试代码的正确性。

  这里要澄清的是,不是说测试代码就不要去拿来测试了,呵呵,测试代码的结果(无论是否测试先行)其实都是一样的,都是可以自动(或者半自动,^_^)执行的测试,我说了,是写测试的目的,由于你的目的不同,中间的过程也是大不相同的,对于开发人员来说,开发代码是最终目的(什么需求分析、系统设计啦等等等等,还不是为了能满足用户需求的代码出来给用户跑嘛,哦,当然,最最终目的,咱还是靠这个养家糊口,sweat),所以开发过程中的单元测试代码,它只是辅助我们开发的(这点好像说了好多遍了,sweat,别丢我臭鸡蛋),所以这些测试代码的最终目的也是为了开发代码,它和测试人员写的测试代码有本质的区别(测试人员要是来看我这篇文章,嘿嘿,SORRY啦,真不是为测试写的)。

  说得好绕口,不过,只有清楚了这点,那么前面2点才会站得住脚,如果一开始得测试代码就只是为了测试目标代码得功能,那么TEST FIRST就是笑话了,连目标都没有怎么可能测试,而根据需求写测试,根本不管内部逻辑以及边界条件等等等等什么这覆盖那覆盖的测试,测试人员一定说我瞎掰了,这也能拿来测试?

  所以,要保证测试代码的正确性,别为了测试而测试,只有不为了测试而测试,你才会真正做到第一、二点。(好像这点应该摆第一去,sweat,不过既来之则安之落)

0
相关文章