UML软件工程组织

简要解析软件测试的认识误区
作者:邓辉 孙鸣 出处:IBM DevelopWorks

由于人们对于软件质量的重视程度越来越高,就导致了测试在软件开发中的地位越来越重要。测试是目前用来验证软件是否能够完成所期望的功能的唯一有效的方法。在这一趋势的引导下,现在很多软件相关的公司都非常重视对于他们所开发的软件的测试,甚至不惜花费巨资购买商用的测试工具,但是效果却不一定理想。究其原因主要是存在着对于软件测试的诸多误解。本文试图对一些比较普遍的关于测试的误解进行剖析,并且在测试对于软件产品质量可能带来的更深远的影响方面,也进行了论述。

测试在软件开发过程中一直都是备受关注的,即使在传统的软件工程中,也有一个明确、独立的测试阶段。随着软件危机的频频出现以及人们对于软件本质的进一步认识,测试的地位得到了前所未有的提高。测试已经不仅仅局限于软件开发中的一个阶段,它已经开始贯穿于整个软件开发过程,人们已经开始认识到:测试开始的时间越早,测试执行的越频繁,所带来的整个软件开发成本的下降就会越多。Extreme Programming更是把测试推到了极限的位置,一切软件开发活动都要从首先编写测试代码开始。

但是,相对于测试这个词的流行程度而言,有关测试教育方面的工作做的还远远不够,很多关于测试的文章都是针对某种测试工具使用方面的,而测试工具厂商也往往出于商业的目的对于测试工具的作用夸大其词。这样,很多的软件从业者就很容易陷入一些误区,导致了测试并没有在他们所在的软件开发项目中起到有效的作用。下面几个小节将对于一些比较具有代表性的误区进行剖析,并对于测试背后所蕴含的一些设计思考进行了阐述,希望能够起到抛砖引玉的作用。

误区之一:使用了测试工具,就是进行了有效的测试

这个误区可以说是一种通病,几乎每一个领域中的CASE工具刚刚出现时都会带来这个问题,比如:如果一个软件开发团队在软件的开发中使用了Rational Rose工具来进行UML图的绘制,他们可能就会声称他们采用了面向对象的方法,也不管他们的设计和代码实际上是多么的过程化。

在测试领域中也一样如此,一个软件开发团队往往认为只要自己使用了某种软件测试工具,那么就应该可以获取测试带来的种种好处,这种想法当然是错误的。因为,要想对一个软件或者模块进行有效的测试,首先该软件或者模块应该是可测试的。可测试性是反映软件质量的一个内在属性,不会因为你使用了某种测试工具进行了测试行为,就使得被测试的软件具有了可测试性。如果被测试的软件本身并不具备可测试性,那么使用多么昂贵的测试工具进行测试所能够带来的收益都是微乎其微的。

巧的是,可测试性和好的软件品质的其他方面有天然的关联,一个具有可测试性的软件必然是一个强内聚、弱耦合、接口明确、意图明晰的软件,而不可测试的软件往往具有过强的耦合和混乱的逻辑。关于可测试性方面更多的内容不在本文的论述范围,请自行参见相关的文献(本文所附的参考文献中有关于可测试性的更深入的信息)。

要想真正获取测试带来的巨大好处,并且使得测试工具能够发挥最大的效率,关键就是要使软件本身具有很好的可测试性。这种能力的获取是一个逐步的过程,是不可能一蹴而就的。最关键的一点就是要不断实践,不断学习一些优秀的经验,不断的反思。要想获取好的结果,就必须要付出努力,这是亘古不变的真理。Extreme Programming中的测试先行的实践倒是一个很好的起点,具体可以参见参考文献[3]。

对于测试工具的选择,只要满足需要并能够自动运行测试用例就可以了。不要一味的追求复杂的功能和不必要的灵活性。对于大多数项目来说,一些非常著名的源码开放的测试工具就足够了,比如:Java方面的单元测试工具JUnit和C++方面的单元测试工具CppUnit。关于验收测试方面,目前没有比较好的满足各方面需要通用的测试工具,不过使用脚本语言,循序渐进的自行开发一个适合自己的验收测试工具也不是一件困难的事情,一句话:只有提高了自身团队内在的素质,外在的工具才能够发挥作用。

误区之二:存在太多的无法测试的东西

在软件开发领域,确实存在一些东西看起来要比另外一些东西难测试一些,但是远非无法测试。并且在大多数的情况下,还是由于被测试的软件本身在设计时没有考虑到可测试性的问题。只不过这种不可测试性不是由于被测试的软件内部的过紧耦合造成的,而是和外部某些很难测试的部分耦合过紧,从而表现出被测试的软件本身很难测试。这些很难测试的部分比较常见的有:图形界面、硬件、数据库等。下面以图形界面为例进行说明。

如果一个软件模块必须要通过图形界面才能够触发它的应用逻辑时,那么要对这个软件模块进行测试时就必须要使用图形界面。但是图形界面又是很难测试的。看起来好像很难办。让我们换一个角度考虑一下,其实我们真正想要测试的是软件模块本身的应用逻辑,而不是图形界面的触发逻辑。如果我们在设计时能够把被测试的软件模块本身进行很好的封装,定义良好的服务提供接口,那么我们就完全可以通过软件模块本身提供的接口进行测试。这样就可以绕过难以测试的图形界面。造成上述软件模块很难测试的原因正是由于在设计时把软件模块本身的应用逻辑和图形界面这两个无关的逻辑耦合在了一起。把这两个逻辑分离,不仅可以对该软件模块进行更容易、更有效的测试,而且也使得该软件模块具有了很好的可重用性和可移植性。

那么对于不好测试的图形界面,我们该怎么办呢?原则很简单,如果某种东西不好测试,那么就让它做肯定不会出错的事情,而把可能会出错的逻辑剥离出来,放到一个可以测试的模块中。对于图形界面来说,就是仅仅保持一个很薄的图形界面逻辑,它的工作就是把用户的请求简单的转发给真正处理该请求的软件模块(一般称之为Application Facade)。转发逻辑足够简单以至于它肯定不会出错,所以我们也无需对它进行测试。关于这方面更多的信息,请参见参考文献[5]。

如果在进行软件开发时能够首先编写测试代码,那么就会迫使你从易使用性,易测试性的角度开考虑问题,从而你就会专注于软件模块的高层抽象和职责。这样就会定义出清晰的、明确反映意图的模块接口来,另外,为了使得测试能够进行,你就会主动把那些导致不好测试的耦合去掉。这样的结果不仅仅是获得了可测试性,并且也产生了更好的设计和系统架构。

?
但是确实存在一些不好测试的东西并且很难只让它执行一些非常简单的逻辑,比如嵌入式系统中的BSP(板级支持包)。开发它们所使用的语言、环境以及它们本身的特性等决定了它们是很难测试的。这里说的难测试其实是一个测试代价问题,具体的说就是要付出更多的时间和努力。这就需要你在付出的代价和测试带来的收益之间进行平衡。如果付出的代价所带来的收益(更少的调试、维护、发布成本)是巨大的,那么付出的代价就是非常值得的。

误区之三:测试代码可以随意写

大家肯定知道测试代码是不能随意编写的,并且在编写测试代码时也不是抱着一种随意的态度,但是你编写出来的测试代码以及测试代码运行的情况却表现出了一种随意性和无序性。因为你可能并没有弄清楚测试的真正意图所在。

本人曾经看到过有关验收测试的这样一个案例,验收测试者使用昂贵的商用测试工具对一个具有图形界面的软件进行测试。测试的方法是通过编写测试脚本驱动鼠标在图形界面上随机的点击(当然每一次的点击,都点到了图形界面上可以接收事件的区域),然后等待着被测试软件的崩溃。当然这种测试方法可以作为验收测试的一个方面,但是决不是唯一的一个方面。还有更重要的内容被忽视了。

测试的目的是用来检验软件系统是否满足了需求。所以,你的测试代码一定要明确的表达出这一点来。就那上面的案例来说,如果测试者真正从用户的需求出发,那么他写出来的测试脚本肯定不会是那样的,而应该是每一个测试用例都清晰的刻画了一项用户的需求,然后检验系统是否实现了用户期望的功能。这样的测试才是有明确目的,才是最有效的测试。和把界面逻辑和应用逻辑隔离,采用明确表现用户需求测试用例进行测试相比,上面的测试方法不能不说是随意了一点。

在针对类进行的单元测试中,也经常会看到一些错误的测试方法。很多的测试者往往针对类中的每个特定的实现细节进行测试。类中的特定的实现细节是很容易变化的,在快速的迭代式开发中更是如此。一旦你测试的类中的某个实现细节发生了变化,你原先的测试代码就要进行相应的更改,否则就失去了意义。这种频繁更改的代价是巨大的。类是一种抽象,它反映了更高层面的内聚性,它应该有明确的职责和定义良好的服务接口,它的职责和对外的接口相对于内部的实现细节来说要稳定的多,并且我们要验证的正是这个类是否具备了它的职责。因此,在对类进测试时,我们应该针对类的接口来验证类是否实现了它的职责而不是其他。这样写出来的测试代码要稳定的多,也有效的多。

细想一下,造成容易陷入针对实现细节测试的原因主要是由于先实现了类,然后才去进行测试。如果先实现了类的功能,然后在去对类进行测试,潜意识中就会不由自主的想去验证已经完成的类的某种实现细节。如果能够在编写实际类前,首先编写针对该类的测试代码,情况就会有很大的不同,因为这会迫使你从类的使用者的角度去考虑问题。结果就是会把关注点放在类的易用性上,放在类的职责上面,放在类提供服务的接口上面,而不是某种实现细节。

总之,测试代码的编写应该从被测试的对象是否满足需要的层面进行,而不是其他。

误区之四:单元测试和验收测试没有什么区别

和误解之三一样,可能很多人并不承认这一点。但是他们却又不能比较清楚的说出二者的差别来。这样,在他们进行测试代码的编写时就会比较迷茫。本小节结试图给出一些区分单元测试和验收测试的一些原则来。

我们还以经常用来和软件进行类比的建筑为例,首先给大家一个感性的认识。单元测试可以类比为一个建筑的质检人员对建筑进行的检测,他关注的重点是建筑的内部结构、地基、框架以及墙壁是否垂直等。他的检测是要保证建筑的各个部分是正常的、安全的,换句话说,就是要保证施工满足建筑上面的质量标准。验收测试可以类比为建筑的使用者来对建筑进行的检测。首先,他认为这个建筑是满足规定的工程质量的,这是有建筑的质检人员来保证的。使用者关注的重点是住在这个建筑的中的感受。他关心建筑的外观是否美观、各个房间的大小是否合适,窗户的位置是否合适,是否能够满足家庭的需要等。这里,建筑的使用者执行的就是验收测试,他是从用户的角度出发的。建筑的质检人员执行的就是单元测试,他是从构建者的角度出发的。

正是这种角度的不同决定了单元测试和验收测试之间的区别。它们是对系统的不同的方面进行的测试,二者是互相补充的。不管我们在系统的构建中使用了多么聪明的方法,不管我们的系统是多么的灵活,但是首先我们的产品必须是可用的,否则我们所做的就是浪费时间,从这一点上来说验收测试要比单元测试显得更加重要。

还以上一小节给出的案例为例,案例中所使用的测试方法仅仅是从系统是否健壮的角度出发进行的,即使系统从不崩溃也不能证明那是一个可用的系统。因为测试根本就不是从用户使用的角度出发的,测试者本应该和用户一起来编写验收测试。单元测试保证我们把事情作对,而验收测试则保证我们做正确的事情。

关于单元测试和验收测试之间的明确划分,没有一个通用的标准,只有通过自己的不断实践来增加这方面的经验。你进行的有关测试的实践越多,你就会越清晰的感觉到单元测试和验收测试之间的界限所在。下面给出一些指导原则,在你编写测试代码时可能会有帮助。

如果一个单元测试要跨越类的边界,那么它可能应该是一个验收测试
如果一个单元测试变的非常复杂,那么它可能应该是一个验收测试
如果一个单元测试经常要随着用户需求的变化而改变,那么它可能应该是一个验收测试
如果一个单元测试比它要测试的代码本身要难以编写,那么它可能应该是一个验收测试

结论

测试是用来保证软件开发过程的高效性,以及保证开发出来的软件产品的高质量和可用性的。软件开发本身就是一件非常困难的事情,这也决定了有效的测试决不是简单的依靠一些测试工具就可以进行的。在使用工具的同时,我们更要加强关于测试的培训、教育,使大家对于测试首先有一个正确的认识。只有这样,我们才能够更加有效、高效的使用工具,才能够使测试真正起到它应有的作用。希望本文能够对大家在进行测试方面的工作时有所帮助。


版权所有:UML软件工程组织