技术开发 频道

软件质量之路(3):测试驱动开发

【IT168 技术文章】

    测试不能够证明错误不存在,只能够证明错误存在。尽可能测试一切可以测试的东西。

    测试是如何驱动开发过程的 

    测试驱动开发起源于XP法中提倡的测试优先实践。测试优先实践重视单元测试,强调程序员除了编写代码,还应该编写单元测试代码。在开发的顺序上,它改变了以往先编写代码,再编写测试的过程,而采用先编写测试,再编写代码来满足测试的方法。这种方法在实际中能够起到非常好的效果,使得测试工作不仅仅是单纯的测试,而成为设计的一部分。为什么这么说呢?

    在编写程序之前,每个人都会先进行设计的工作。可能有些人的设计比较正式,绘制模型,编写文档。有些人的设计只是存在于脑海之中。且不论是设计是精细还是粗糙,你都为随后的编码活动制定了一个标准。这个标准的明确程度和你的设计的细致程度有关。但应该承认,这个标准是不够细化的。因为你的设计不可能精细到代码级的程度。而标准不够明确的则会产生一些问题,例如,在编写代码的过程中,你还可能会发现原先的设计出现问题,从而中途改变代码的编写思路。这将会导致成果难以检验,进度难以度量。

    既然以设计为导向的标准不够明确,不够具体。那什么样的标准才是合适的呢?只能是代码。因为代码是最明确、最具体的。所以测试优先的本质其实是目标管理。编写测试代码其实是在制定一个小目标。这个小目标非常的明确,它规定了你需要设计的类、方法。以及方法需要满足的结果。这些目标制定完成之后,你才开始编写代码来达成该目标。测试的目标要比设计的目标粒度更小,但是成本上却更为经济。其原因有四:

    *细粒度的设计需要花费大量的成本,虽然CASE工具都提供了代码自动生成的功能,但结果往往难以令人满意。所以,设计如果要做到和测试相同的粒度,成本不菲,如果粒度不够细,指导的意义又不够。
    *减轻了测试的工作量。无论是否进行设计工作,测试工作都是不可避免的,先进行单元测试,可以减少后续的测试工作量。
    *采用测试优先的过程中,设计的粒度较大。因为测试可以实现一部分的设计工作。这样,设计上可以节省一些工作量。例如,你不再需要将类图细化到每个方法。
    *在编写测试代码上花费的成本,会在回归测试上得到回报。自动化测试的最大好处就是避免代码出现回归。两相权衡,编写测试的代价其实不高。

    你也许会说,我既不进行如此精细的设计,也不事先编写测试代码,这样的成本不是最低吗?请注意,我们的前提是在讨论高质量的软件设计。在一些规模较小或是开发人员能力极强的项目中,确实可以如此办理。但是对于强调质量的大项目,这种处于混沌状态的开发思路是不可取的。

    测试优先是软件开发中一种细粒度的目标管理方法,通过明确的目标,推动软件开发的进行。

    在业界中,采用测试作为评价软件标准的做法是非常常见的。例如,sun公司就专门设计了测试软件,对各个实现J2EE规范的产品进行测试。使用测试作为规范的最大好处就是明确、具体。

    使用测试代码建立目标,编写代码完成测试目标,再制定下一个目标,如此循环,构成了测试驱动开发的工作流程。在接下去的篇幅中,我们开始讨论测试驱动开发中需要注意的一些问题。

    测试必须是自动化的

    和自动化测试相对的是手动测试。手动测试有着自动化测试所没有的优势。最明显的就是简单。任何一位开发人员都能够进行手动测试。即便是用户,也很容易掌握手动测试,他们输入数据,并观察软件的反应(输出),从而判断行为是否正确。大部分的手动测试都是对输入和输出的检验,是一个端到端过程,很能说明问题。

    但是手动测试存在的问题比它所带来的好处要多得多。手动测试可能引入错误,人为的输入错误很容易发生,尤其在数据量大的情况下;大量重复性的手动测试可能成本较高,如果考虑软件发生改动而需要重复手动测试的情况,这个成本还会更高;手动测试的覆盖面不广,只能够测试系统的输入和输出;没有办法对组件进行隔离的测试,从而导致发现问题和解决问题的成本都太高。

    基于上面的讨论,我们应该看到,测试应该做到自动化。虽然一开始测试自动化的成本较高,但是从整个的开发过程来看,自动化测试所产生的价值远远超过其成本。
   
    自动化测试的范围

    那么,到底有哪些东西是需要纳入到自动化测试的范围的呢?例如,对于一个典型的分层应用来说,就有数据库层、数据库访问层,业务逻辑层、界面控制层、界面层。这些层次的测试特点各不相同,哪些应该进行自动化测试呢?最理想的情况是全部。测试一切可能是测试的基本原则,让一切测试都变成自动化则是测试驱动开发的准则。

    应该承认,建立自动化测试需要付出成本,有些自动化测试成本较低,有些则较高。例如,对业务方法的自动化测试相对容易,对关联到数据库的业务方法的测试则繁琐一些,因为你需要处理更多的情况。而界面的自动化测试则较困难,因为界面涉及到大量的人机交互,手动测试是非常简单的,但是自动化测试则相当的困难。

    那么,我们就来看看,像界面测试这样的成本高昂的测试需不需要进行自动化呢?我们拿驻留的Web界面来作为人机交互界面的范例。

    首先,按照分层的原则,界面的层次上不应该拥有业务逻辑,界面层负责的事情是收集用户的动作,将用户的动作请求委托给后端的业务层,并对动作进行响应。所以,和业务相关的逻辑已经被剥离到了业务层了。它的自动化测试属于业务层。同时,我们还发现,在测试的推动下,软件的结构变得更加的合理。

    其次,虽然业务逻辑大量的迁移出界面层,但是界面层中还有状态,还有控制逻辑。这些因素都是和界面的控制的表现息息相关的。既然有逻辑,就需要测试。根据MVC的思路,界面层中包含了模型、视图和控制器。模型是对业务层数据的封装,在J2EE的应用中,可能是普通的JavaBean,也可能是离线的数据封装,或是简单的数据集合。视图负责表现模型,而控制器的职责则比较多,它需要负责处理和检查请求参数,负责调用业务对象并传递请求中所包含的数据,负责创建模型,负责生成视图并把模型传递给它。

    所以,在一个真正的MVC界面设计中,测试的重点在于控制器。模型的测试是很容易的,大部分的模型仅仅包含了数据,所以甚至都不需要测试。一个完美的视图,它应该没有包含任何的逻辑,仅仅只是将模型以某种方式表现出来而已。一个设计优秀的视图,可以很容易的进行替换,而不会造成任何的影响。例如,一个JSP视图可以用一个XSLT视图进行替换。所以,结构合理的视图也是不需要测试的(但对页面要素的检查是必要的)。注意,这里的讨论也同样表明,测试驱动设计向合理的方向发展。

    大部分的控制器都是基于servlet技术的,servlet技术是典型的容器内应用,这使得Junit不容易使用。而Cactus(http://jakarta.apache.org/cactus/index.html)能够解决这个问题,它对Junit进行了扩展,提供了大量的预置功能,简化了request、response、session的使用,Cactus通过Redirector Proxy,建立了一个测试客户端和测试服务端模型,来实现基于容器内的测试。下图是典型的Servlet的结构图:

    Cactus的详细工作机理可以参考这里(http://jakarta.apache.org/cactus/how_it_works.html)。

    除了构造一个容器来进行测试之外,另一种方式则从根本上改变了控制器的测试方法,那就是不将控制器实现在Servlet技术之上。这样做的好处是测试无需依赖于容器。典型的如springframework(http://www.springframework.org)的Web层就是这样设计的。

    无论采用何种方法,都说明软件业在对界面的自动化测试已经开始充分的关注,并提出了不少的解决方法了。这样做的目的无非是两点:一是通过自动化测试提高软件质量,二是通过对测试的关注来推动设计的优化。

    最后,从开发文化上来看。界面自动化测试的要求意味着开发人员需要先和用户进行充分的沟通,绘制出满足需要的页面,这其实是原型方法的应用,对开发过程是有利的。此外,开发人员需要慎重的思考页面的设计,保证页面设计的抗变性和可扩展性,否则,你会发现测试代码变得非常的不稳定,从而导致一些不必要的麻烦。这种文化将会推动设计的发展。

    所以我们看到,在一个成本较高的自动化测试领域,通过合理的设计和引入工具,可以降低自动化测试的成本。而且,在上述的讨论中,我们也发现,之所以自动化测试的成本高昂,往往是由于设计不当而造成的。在界面混杂大量的逻辑,导致变化不断发生,不但代码需要修改,测试代码同样需要修改,设计的随便才是高成本的真正罪魁祸首。也正是因为这个原因,测试才能够驱动设计的优化。 

    测试的分类

    单元测试

    单元测试是典型的对代码逻辑的黑盒测试。在测试驱动方法中,不太强调白盒测试(绝大多数的白盒测试都是通过评审进行的)。这样做的好处是关注接口胜于关注实现,这是一种分析复杂软件的有效办法。这一点我们在后续的文章中还会讨论。

    单元测试是开发人员的职责。一般来说,测试的编码最好由不同人来负责,避免出现盲点,以提高测试的有效性。但是单元测试的粒度很小,如果进行分工,沟通的成本会相当高。此外,采用测试优先的实践,对测试进行适当的培训,也能够有效的降低单个人的盲点范围。

    单元测试可以加入到小组日构建中,也可以不加入。如果不加入,那么需要有一种机制来管理单元测试活动。

    集成测试

    集成测试的粒度和测试的范围要比单元测试大。我们拿数据库测试来做例子。现在需要对一个业务对象进行测试,它需要用到持久化机制。在单元测试中,我们将不涉及数据库而单独对业务对象进行测试(使用MO技术,下文中讨论);但是在集成测试中,我们需要将数据库的数据一致性也纳入进来,所以测试包括数据库数据的建立,测试业务方法,使数据库恢复原状。

    集成测试应该是日构建的重要组成部分,即日构建标准中的测试标准。最好将集成测试交给QA部门负责。QA部门的精力可以放在使用或编写一些工具(Cactus就是典型的集成测试工具),建立标准的测试数据,安排测试计划等活动上。

    接受测试
   
    有时候很难区分集成测试和接受测试。接受测试的关注点是用户。用户通过将数据输入系统来观察系统的输出。所以,了解用户的需要并将用户的需要转换为接受测试是接受测试中最关键的工作。接受测试处于测试过程的最后环节,是判断软件是否满足用户需求的试金石。毫不例外。接受测试也应该是自动化的。例如,HttpUnit就是一个自动化接受测试的工具。另外,很多的专业测试工具提供的自动化的脚本测试工具也属于这个范畴。

    测试推进设计

    在界面测试的讨论中,我们已经认识到测试是可以推动设计的。在这里我们打算结合具体的技术来讨论测试和设计之间的关系。

    针对接口设计

    测试驱动方法采用的是黑盒测试,为了保证测试的稳定性,被测代码接口的稳定性是非常重要的。否则,变化的成本就会急剧的上升。所以,自动化测试将会要求你的设计依赖于接口,而不是具体的类。这种设计目前被公认是较好的设计思路。

    MO技术

    这里不会介绍MO(Mock Object,伪对象)技术,DW专区中已经有这方面的讨论文章了。这里的重点是讨论如何正确的使用MO技术。用过MO的人都有这样的疑惑,MO技术太麻烦了,编写大量的伪对象仅仅是为了配合被测试对象的测试工作,未免有些小题大做了。

    实际上,MO技术最大的好处并不仅仅是测试本身,MO技术将推动设计向着针对接口,可抽换的方向发展。因为MO技术需要做到伪对象和实际对象之间能够平滑的切换,这个能力对于软件架构来说是非常重要的,它充分表现了架构可扩展性和抗变性。

    当然,在实际中,MO技术确实需要增加大量的代码。有一些工具能够简化你的工作,例如EasyMock等一些工具。

    测试覆盖率


    Unit tests drive code quality;

    Clover drives unit test quality.

 
    上面的两句话是Clover(http://www.thecortex.net/clover/index.html)的广告词,扣除广告的意味,这句话说得还是非常有道理的。我们的引言中就说明,测试没有办法证明错误不存在。所以,对测试进行分析是有必要的,而测试覆盖率就是最重要的一个指标。

    测试覆盖率分析有两种方法:代码覆盖率和分支条件覆盖率。对测试进行覆盖率分析是软件度量的重要方法,也是测试的组成部分,不少的开源项目已经将其纳入到了日构建的过程中。

    测试的成本

    虽然之前我们讨论了大量激动人心的思路和技术。你可能会热血澎湃的打算在组织中实施测试驱动方法。但是我不得不向你泼冷水了。测试驱动方法的引入不是简单的过程,对一些企业来说,甚至相当难以做到。这是因为以下这几个原因:

    工作量的估算方式需要改变。在测试驱动方法中,一个开发人员除了需要编写实现代码,还需要编写测试代码,这将会使得工作量上升,此外,为了自动化测试而对设计的改进还将会需要一定的时间。所以,要求开发人员学习测试驱动方法没有任何的意义,关键是需要为他们留出足够的时间。

    项目进度。由于工作量的上升和新知识的使用,项目进度会迅速下落,然后随着开发人员熟练程度的提升和自动化测试的优势才会慢慢回升,如果实施成功,最终的项目速度将会超出实施前,这是完全有可能的。

    人员的主动性和勇气。根据我们的经验,不少的组织和开发人员都能够认识到测试驱动的好处,但是往往由于现实环境的原因,导致测试驱动方法的实施无以为继。组织由于项目的时间压力,导致其不敢对测试驱动方法进行推广,往往是浅尝则止。个人由于缺乏足够的耐心和时间,导致其不愿和不敢对设计进行重构,而这恰恰是测试驱动的前提。

    测试驱动方法的应用没有那么简单,一个组织如果没有足够的勇气是很难做到的。所以,为什么一开始我们就强调,本文的读者是那些应用较负责,对质量非常敏感的项目的相关人员,其中的一个原因就在于此。

    建立测试文化

    测试驱动方法不是一个简单的方法论,它也不会和任何的方法论进行竞争。事实上,无论你的组织采用何种方法或过程,都可以从测试驱动中获利。因为它强调的是质量文化。
   
    把测试看作一项核心工作,测试同样需要重构,以及必须的文档。

    固定测试的目录组织和包组织。例如,一种较好的组织测试的方法是采用和源代码同样的包名,但处于完全不同的目录中。

    使测试成为日创建的核心步骤。

    测试是所有人的事情,而不仅是QA的事。

    进一步了解

    从Junit的网站(http://www.junit.org)上,你可以了解到Junit的大量信息,包括介绍性的文章,Junit的扩展等。

    Test-Driven Development Series Part 1 - Overview (http://www.theserverside.com/articles/article.jsp?l=TestDrivenDevelopmentPart1)是一组介绍测试驱动开发的文章,和本文不同,它更侧重于实际代码的编写。

0
相关文章