测试观念谈
邓 辉
到目前为止,测试仍然是一种公认的检验程序正确性最为有效的手段。详尽的测试可以大大地降低程序的缺陷率。虽然缺陷率是目前一种公认的检验程序质量的指标,但是它只是一个结果,要到达大家都满意的一个指标值是要付出一定的代价的。
糟糕的是,有很多项目在进行测试时,都只把注意力放在了这个缺陷率和覆盖率结果上,他们编写了非常详尽的测试用例文档,然后根据这个用例文档编写出对应的测试代码进行测试。这种做法在软件需求稳定的情况下也许能够工作的不错,但是需求不断变化这个大家都不愿意看到却又无法避免的现实,给这种测试方法带来了巨大的打击。功能的增加、修改和bug的修正都不可避免地导致程序结构的变化,而这种只关注覆盖率的测试用例文档和测试代码必然严重依赖于程序的结构。结果自然不难想象,他们不仅要去更改程序结构,还得去更改测试代码,即使在所要求的程序功能没有改变时也是如此。更加糟糕的是,他们还得去更改测试用例文档,以保持其和测试用例代码以及要测试的代码一致。否则的话,这份文档就会变得过时,此时非但不会具有什么用于交流的好处,反而会变得危险。
为什么会出现这种结果呢?难道说他们关注覆盖率错了吗?关注覆盖率本身没有错误,但是由于他们把覆盖率、缺陷率和软件其他特性(其中最为主要的一个就是封装性)的关系割裂开来,而这种割裂必然会带来高昂的测试代价。一个具有糟糕变化率封装特性的代码,很可能不论你付出多少代价,也根本无法达到高的测试覆盖率。这就是软件熵的惩罚。
所以我们要想在保证覆盖率和缺陷率的前提下,降低测试的代价,进行有效的测试,我们就必须把关注点放在软件的其他内在质量上面,我认为最为重要的几个指标是:封装性水平、变化率隔离水平、依赖关系处理水平以及是否满足Once and Only Once原则。这些指标会大大增强软件的灵活性和易更改性,并会大大提高软件本身的可测试性,直接降低到测试的代价和质量。
其实,为了能够和前面提到的需求易变特性相适应,很多探索者除了提出一些有助于达成上面所说的质量指标的技术(比如:OO、AOP、IOC以及语言的动态性等)外,还提出了很多在软件开发时可以遵循的一些过程方法,其中迭代法是目前得到普遍认可的一种方法。同样,我们所掌握的技能直接决定了我们想要实施的过程方法会带来的结果。一个没有掌握OO技能的团队,很可能会在实施迭代法时受到严重打击。因为和测试一样,他们缺乏保持软件“软性”所需要的足够技能。
说了这么多,我主要想表达的就是希望我们不要孤立地去谈测试,而是要认识到开发者本身的开发技能和测试有效性之间的关系。提高测试有效性(包括提高过程实施的有效性)的根本途径就是提高开发人员的技能。这个技能不是他们编写代码快慢的能力,而是他们对于好的代码和坏的代码的识别能力。这样才能非常经济地从根源上大幅提升软件的质量和开发生产力。我们在选择过程方法和实践时,要根据自己团队的情况,有针对性的选择那些对提升团队当前开发技能有帮助的部分。
那么团队应该如何开始呢?我觉得TDD(Test-Driven Development)是一个很好的起点,通过TDD的实践,我们可以提高我们的分析、设计能力,改善编程的风格(注意不是编程规范),可以提高我们的快速迭代和节奏控制能力,可以提高我们对code smell的识别和重构能力,最为重要的是可以提高我们对需求的理解和快速验证能力(也就是我们做正确的事情的能力)。这些能力是我们能够更为有效地实施测试和迭代所必须的。敏捷方法中有很多实践存在争议,但是TDD是唯一一个得到广泛认可的实践。最近MSDN TV在对《Code Complete》(第2版)的作者Steve McConnell采访时,问到如果作者要对《Rapid Development》一书进行修订的话,会更改那些内容,Steve McConnell说,到目前为止唯一会更改的就是增加进TDD这项最佳实践。
TDD是一个很大的话题,我这里就不再赘述。但是有一点一定要记住:TDD is not about Testing。作为结果的测试套件,只是一个副产品。更为详细的信息可以参考Kent Beck的《Test-Driven Development》和Robert Martin的《Agile Software Development》。另外,关于测试认识方面更为详细的信息,可以参考我去年在IBM developerWorks上发表的《软件测试认识中的误区》一文。
谈到TDD,我不禁想起了前两天和两位朋友探讨覆盖率的必要性方面的问题。一位朋友最后举出了一个例子,Kent Beck在《Test-Driven Development》一书中说TDD实施的结果必然是100%(或者接近)的测试覆盖率。其实,在谈论TDD时,一般是不会讨论测试覆盖率的,Beck先生这样做只是为了和传统的理解有个可比性。为什么实施TDD后,会很容易地达到100%的覆盖率呢?原因是这样的:TDD使你关注于职责测试,在进行TDD时,你得不断重构你的代码使之符合好的原则,你得不断消除代码中的味道。最终的结果,你得到的是一个职责单一,没有重复,接口正交、紧凑,方法短小且不含(或者很少)分支的类(模块)。而你又是基于职责测试的,想不达到100%都难。是的,好的代码必然会很容易地带来足够高的测试覆盖率。
下面,我想谈一下对于想在公司范围内进行测试推广方面的一些建议,仅供参考:
l 在测试用例文档化方面,我持反对意见。我认为测试用例代码本身就是可以运行的文档,而且永远和实际的代码保持同步,并且最为直观、清晰且没有歧义。当然前提是我们必须要遵守TDD的纪律。如果真的需要书面化的文档(这种情况很少会发生),我们可以通过self-documented的代码来自动生成,比如在Java中可以使用JavaDoc工具。当然一些高层次的说明性文档除外。
l 可以把覆盖率作为一个指标,但是不应该把它作为唯一的指标。应该辅以其他更多纬度的度量标准。最重要的是要时刻关注高覆盖的代价,如果代价很高,那肯定是代码本身质量不高。
l 不应该以开发人员目前水平不高为由,就只实施一些看起来“安全”的实践。我们应该实施那些能够提升开发人员技能,能够使得他们变得更加优秀的实践。在我们拒绝实施一些推荐实践时,我们不应该只以“我们不希望你这样做,因为我们害怕你会偷懒”作为判断依据。