内容


在编码之前进行测试

Comments

插图以前的文章我讨论了匹配项目和流程的重要性,并劝您考虑一下您所构建的软件环境。这提示我去考虑软件开发是否拥有真正通用的最好实践。我认为不会有。对于现有的实践来说,总是存在至少一个环境,在该环境中,实践不是最佳的。然而,一些实践在大多数情况下是非常好的。其中一种——优先测试的程序设计,或称为 TFP——是本文的主题。

项目范围和流程灵活性

在开始对此实践进行检验之前,我们需要一些关于什么时候使用它的背景知识。要理解这一点,让我们了解一下项目范围和流程灵活性之间的关系。在这几年与客户打交道的工作中,我注意到,随着项目范围的扩展,对统一的形式化流程的需求也成比例地增加(参见图 1)。大多数有经验的开发人员积累了了大量的实践和技术,并可以根据工作内容有选择地应用他们。当他们独自完成任务时,他们可以以自己的方式工作。但是,如果他们在团队中——特别是在企业级——项目经理必须设立更高级别的流程以保持整个团队工作的一致性。通常,该流程必须是相当地稳固的,因为背离它将影响到许多人。

图 1:项目范围与流程灵活性之间的关系

图 1:项目范围与流程灵活性之间的关系

图 1:项目范围与流程灵活性之间的关系

例如,让我们假设您是软件架构师,总是按照软件需求规范(Software Requirement Specification,SRS)来将项目功能需求描述成一列不相干的“应该有”的条目。但之后您参加了一个关于用例的专题讨论会,并认识到用例更加正确地描述了为什么系统将对涉众具有意义。尽管您毫无疑问地会采用更好的技术,但是切换到当前项目的用例会导致混乱——除非您能够确保团队中的每个人也进行了切换。

当然,如果您在处理一个大项目中的小项目,您可能会采用不同的技术来优化您的工作,只要这些技术不会干扰更广泛的组织实践。例如,如果大项目要求为用户界面使用专门的 2-D 和 3-D 图形,为绘制这些图形而实现类库的子项目可能采取特殊方法来描述需求。 这不会对其他流程的活动产生负面影响。另一个适于模型驱动开发的子项目可能使用 UML 模型来生成子项目代码的重要部分——没有妨碍整体项目代码的生成规程。

优先测试的程序设计:开发人员的个人实践

当他们在小项目或子项目中用 他们的方式灵活地工作时,一些开发人员选择应用极限编程(Extreme Programming,XP)1方法的理论。本文不会讨论对项目“进行 XP”的意思,本文将着眼于 TFP,即与此方法论(XP)相关的实践,我认为 TFP 帮助许多软件专业人员创造更好的工作。尽管您依照规范来应用并完善 TFP,我发觉 TFP 很容易理解并且很适合我的工作方式。它还很容易向我的学生示范,并且我已经教授了学生如何在学期项目中利用它。

下面的说明是依据 如何应用 TFP 的经历阐述的。尽管这些不是详尽的说明,但它也许会提供足够的信息激起您的欲望并鼓励您学习更多的关于 TFP 的内容。为了达到这个目的,我在 参考资料部分提供了许多资源。

用于单元测试的实现流程

TFP(也被称为测试驱动设计或测试驱动开发)实际上是用于实现 XP 所描述的单元测试实践的流程,该流程简单地描述为: 在您写代码之前,确保您拥有失败了的测试。换句话说,写一个将您要撰写的代码的测试。

Ron Jeffries 将流程描述如下:

  • 找出您不得不做的事情。
  • 为需要的新功能撰写单元测试。注意您所考虑到的新功能的少量增加。
  • 运行单元测试。如果测试成功,您就完成了。回到步骤 1,或者如果您完全地完成了,就可以回家了。
  • 处理直接的问题:也许您还没有撰写新的方法。也许方法并不管用。不论怎样都要处理。回到步骤 3。2

这似乎非常简单,但您需要满足两个需求才能够成功地采用 TFP。首先,您必须了解如何书写一个好的测试。第二,您必须要求自己在编码之前书写测试。这可能与您在培训时所学到的背道而驰。

什么是好的测试?

在确定要测试的内容后,Jeffries 说,“选择您能想到的所增加的最少的新功能。”这是个好建议。大多数初学者都试图在每个单元测试中填入太多的要素。然而,最少的增量也不总是最好的选择。当您对书写测试很有经验时,您会发现对您来说什么是最舒适最有效的。

对于我来说,一个好的测试取决于一样东西。然而,如何定义“一样东西”要根据我想编写代码的类型而不同。这和设计方法和类的原则类似。您想要整个设计具有高凝聚力,测试也应当具有结合性。

用于 TFP 的工具

要实施 TFP,您需要有自动化的工具。基本上,TFP 的循环是测试/编码/调试/重新分解/重复。如果没有工具来支持这些活动间的快速转移,您很快就会受挫并放弃了。

支持 TFP 的典型工具是良好的集成开发环境(integrated development environments IDEs)和单元测试框架。我使用带有 JUnit 插件的 Eclipse IDE 来进行所有的 Java 开发。

JUnit 是由 Kent Beck 和 Erich Gamma 为测试 Java 程序而设计的单元测试框架。3JUnit 的变化版本支持对其他语言程序的测试,例如用于 C++ 语言的 cppUnit。在本文下个部分,我将用 JUnit 来创建实例。

使用 TFP 的实例

用于软件测试的类和程序经常使用一个示例程序,程序接收三个数值,然后将这三个数值作为边长来测试三角形的类型。如果您用三角形的角度进行测试,那么可能的类型是直角、锐角、钝角或等角三角形。如果您通过三角形的边进行测试,可能的类型是等边、等腰或不规则三角形。要示范如何使用 TFP,我们将创建此示例程序,并包含名为 TriangleTester 的类。4在书写测试前,TFP 要求在空的工程中首先创建测试类。然而,我发觉首先创建带有一些空方法的类文件,然后生成测试文件是很容易的。 集成在 Eclipse 上的 JUnit 可以自动生成测试类,因此我让工具为我生成,从而减小我的工作量。代码示例 1 显示了我所创建的 TriangleTester.java 文件,其中包含了一个空的 kindOfTriangle 方法。

代码示例 1: 方法 kindOfTriangle 的框架

代码示例 1: 方法 kindOfTriangle 的框架

代码示例 1: 方法 kindOfTriangle 的框架

在此处我必须做出几个决定。首先,我需要决定用作参数的数据类型。我选择最一般的类型,双精度实数。我还要决定如何表示三角形类型。经过考虑,我创建了名为 TriangleType的类型,表示三角形的属性。

由于不存在 TriangleType 类,我需要创建该类。该类如代码示例 2 所示,它只有一个构造函数。

代码示例 2:空的 TriangleType 类

代码示例 2:空的 TriangleType 类

代码示例 2:空的 TriangleType 类

现在我准备书写第一个测试。Eclipse 可以为TriangleTester 类生成测试类。按照默认设计,将该类被命名为TriangleTesterTest,但您可以将名字改成您想要的名字。由于在 TriangleTester 类中已经有了一个方法(kindOfTriangle),所以我利用 Eclipse 的 JUnit 集成部件生成一个测试方法,testKindOfTriangle,与原有方法对应。注意到所有 JUnit 测试方法必须以单词“test”开头。测试用例不重要。

很多测试用例都可用于测试kindOfTriangle。我需要决定对不同的测试用例是使用一个测试方法还是多个测试方法。我喜欢使用许多小的测试方法。因此,我将自己书写每一个用到的方法,不让工具生成原始的测试方法。

首先应该测试什么呢?如我上面所提到的,我们可以将三角形的类型按照角度或边来分类,在某些情况下,我们提供的三个数值也许不能组成三角形。先选择哪种可能的情况无关紧要,因此我选择直角三角形。

创建名为 testRightTriangle 的方法并书写一个简单的测试,如代码示例 3 所示。

代码示例 3:直角三角形测试

代码示例 3:直角三角形测试

代码示例 3:直角三角形测试

勾股定理(Pythagorean Theorem)告诉我们边长为 3、4 和 5 的三角形是直角三角形,因此可以使用边长 3、4 和 5 来简单地测试。5甚至在将 junit.framework.Assert 类导入之后,测试类还是不能编译。这是因为TriangleType 类还没有名为isRightTriangle 的方法,需要创建一个。

当实施 TFP 时,只添加了足够让测试通过的代码。在这种情况下,我需要真实值,要做到这些的最简单的方法是加入代码示例 4 中的代码。

代码示例 4:原始的 isRightTriangle 方法

代码示例 4:原始的 isRightTriangle 方法

代码示例 4:原始的 isRightTriangle 方法

您可能怀疑是否应该为 TriangleType 类书写测试类。在这一点上我选择不写,因为在TriangleType 类中书写的每个内容,TriangleTesterTest 都进行了测试。

现在执行我的第一个测试。在 Eclipse 中执行测试非常简单。选择TriangleTesterTest 类并让 Eclipse 来执行该类(作为 JUnit 测试),当执行时,该测试失败了。图 2 显示我截取的 Eclipse 中的 JUnit 视图。红色条说明失败,视图中其余的信息帮助我确定发生错误的内容。

图 2:第一次运行测试后的结果的 JUnit 视图

图 2:第一次运行测试后的结果的 JUnit 视图

图 2:第一次运行测试后的结果的 JUnit 视图

点击放大

现在我知道问题所在了。kindOfTriangle 方法返回 null。将返回值改为TriangleType 实例对象并重新执行测试。现在显示条变为绿色,如图 3 所示。

图 3:修改代码之后的 JUnit 视图

图 3:修改代码之后的 JUnit 视图

图 3:修改代码之后的 JUnit 视图

接下来要做什么?考虑用非直角三角形?如果输入不同的数,是否还是直角三角形?在写好的测试方法中添加一个关于此问题的测试,如代码示例 5 所示。

代码示例 5:添加对非直角三角形的测试

代码示例 5:添加对非直角三角形的测试

代码示例 5:添加对非直角三角形的测试

现在,当运行测试时,在刚添加的部分出现声明错误。原因很简单。现在isRightTriangle 方法返回的值是true(代码示例 4)。我需要在写入新功能前对此进行修改。直到现有的代码能够通过先前的测试,才能加入新的代码。首先,添加代码,检查三角形是否为直角三角形,如代码示例 6 所示。

代码示例 6: 在主测试中检查是否为直角三角形

代码示例 6: 在主测试中检查是否为直角三角形

代码示例 6: 在主测试中检查是否为直角三角形

现在,我自然要为isRightTrianglesetRightTriangle 书写代码。分别地将方法 isRightTriangle 添加到 TriangleTester.java 中,将方法setRightTriangle 添加到 TriangleType.java 中,如代码示例 7 和 8 所示。

代码示例 7:用于检验直角三角形的代码

代码示例 7:用于检验直角三角形的代码

代码示例 7:用于检验直角三角形的代码

将添加的直角三角形的属性设为私有变量,并增加设置方法。

代码示例 8:保护直角三角形的属性

代码示例 8:保护直角三角形的属性

代码示例 8:保护直角三角形的属性

此时 JUnit 中再次出现绿色条,因此我可以继续添加新功能。还应该继续测试其他类型的三角形吗?也许,但在某种程度上,我必须处理实数运算在计算机上是不精确的事实。我知道这要凭经验。所以此时我想看看到现在为止我所书写的代码能否正确处理实数。(我肯定代码不能正确处理,但需要用测试来确定)将代码示例 9 中的测试添加到直角三角形测试中。毫无疑问,JUnit 显示出红色条:测试失败。

代码示例 9:对实数精度的测试

代码示例 9:对实数精度的测试

代码示例 9:对实数精度的测试

现在我要做出一个设计的决定。如何处理精度问题呢?我所能想到的所有解决方案都需要重新分解,返工。经验表明一些返工是不可避免的。至少我现在有一组要求代码必须能够通过的测试,到现在为止,我还没有进入需要大量返工的编码阶段。6需要一个delta——可接受的算术错误的量。我想让客户程序测试保持原样,因此添加了默认的 delta,删掉了直角三角形测试的返回语句,并加入了代码示例 10 中所示的代码到 TriangleTester.java 中。

代码示例 10:默认的精度 delta

代码示例 10:默认的精度 delta

代码示例 10:默认的精度 delta

然后,我继续添加测试,每次通过添加新代码、变更现有代码及重新分解的方式来修改代码。最终,实现了一个完整的类,我对之很有信心。我还有可以随时运行的测试。将来我如果改变代码,或其他人改变代码,这些测试会告诉我们是否我们破坏了现有的功能。

在这里,我将停止向您描述所有细节。在继续之前,看看表 1,显示了我所写的测试并按照我所写的顺序排列。还要了解一下在代码改进过程中我所必须做出的决定。

表 1:按创建顺序排列的测试
测试描述
有效的直角三角形进行检验,以确保将边长为 3、4 和 5 的三角形识别为直角三角形。
非直角三角形进行检验,以确保不将边长为 3、3 和 5 的三角形识别为直角三角形。
实数精度向程序中加入实数默认 delta。
可变的默认 delta允许客户更改默认的 delta。这致使我将TriangleTester 的方法设为非静态的,要求测试程序要有一定变动。
将 delta 作为方法调用的一部分来进行测试 让调用程序提供 delta 值,作为对kindOfTriangle 方法进行调用的一部分。
对无效的三角形进行测试进行检验,看传送给 kindOfTriangle 的数值是否代表三角形。如果不能,返回特殊的TriangleType 类型。
测试,以保证不能对 NOT_A_TRIANGLE 进行修改NOT_A_TRIANGLE 是一个常量,但没有对它进行保护,以防止更改。为保护实现特殊 TriangleType 的代码,创建 TriangleTypeTest.java
对直角三角形边的组合进行测试确保程序能够识出直角三角形,不论哪个边是斜边。
等腰三角形测试确保能够识别出等腰直角三角形(等腰且直角)。这与直角三角形测试类似。
等边三角形测试与先前的测试类似。

我用了大约 100 行 Java 代码和 70 行测试代码实现了 TriangleTesterTriangleType 类。

TFP 的好处

采用 TFP 会有许多好处,我已经感受过一些好处了。您的工作风格和环境将决定您从实践中得到多少价值。

测试您的代码

TFP 最明显的好处就是,它为您写的代码提供测试。让我吃惊的是,今天有多少程序员在编写代码时不去考虑测试。也许一些组织没有清楚地表述出他们对最终代码的质量期望值,许多组织运用了将整个测试负担加到质量保证组上的流程。

事实上, 每个人都要对质量负责——不论您如何定义。要求程序员在将代码加入项目其他部分中时要进行单元测试,这种想法是合理的。如果程序员使用 TFP,他们就不得不对代码进行测试。没有什么办法可以省去测试。刚刚学习如何测试代码的学生经常认为 TFP 不一定会生成好的测试。我告诉他们,必须判断是否“坏的”测试会比根本不测试要好。也许我们将在未来的专栏中探究此问题。

三角形测试实例一共有七个测试方法,但有二十三个截然不同的测试。这还不是全集。我还可以加入更多,但我相信我所开发的测试非常健壮。如果没有书写任何测试,我不会有这样的信心。

高比例的覆盖面

您可以通过许多方法来测量代码覆盖率:通过评估代码行或语句的覆盖面、条件覆盖面、分支覆盖面等等。当采用 TFP 时,可以实现 100% 的代码覆盖率。尽管我的那些使用了 TFP 的特殊方法没有提供 100% 的覆盖率(参见下面的多少量足够?部分),但该方法为可能包含缺陷的代码段提供了可以接受的覆盖水平。

测试人员赞同,大约 80% 的代码覆盖率是可以接受的水平。试图实现完全承保常常是浪费时间,如果您花上几个小时去测试错误情况和异常情况,您的受益会减少。

理论上说,如果为满足现有的测试而撰写代码,那么您肯定能够实现对您所写的 行代码的覆盖。然而,这不太实际。我没有见过任何实验研究证实有 100% 的覆盖率。

重点是,采用 TFP 可以确保您在单元测试中实现相当大的代码覆盖率——可能远远超过我今天所达到的。

测试驱动的设计

设计在发展,我通过经验了解到这一点。当业务环境改变或涉众提出新的需求时,我们需要一些方法来修改现有代码。这就是软件为什么要且灵活的原因。

当使用 TFP 来驱动您的设计时,实际上您已经采用了测试驱动设计(test-driven design,TDD)的方法来构建具有更加简单更加灵活的体系结构的软件。

在上面的三角形测试实例中,我按照自己书写的测试修改了设计。在测试的响应中添加了常量 NOT_A_TRIANGLE。根据最终的结果,我可能决定去掉常量并向代表无效三角形的 TriangleType 类中加入一个字段。设计将会发展。

当实现对等边三角形和等腰三角形的测试时,我认识到我只用了 delta 值来用于直角三角形的计算。也许我会加入“误差因素”,和 delta 一起来确定是等腰三角形还是等边三角形。但是,由于我现在不需要,所以我可以将它推迟到另外一天——我也许真的需要它的时候。

TFP 或 TDD 是用来补充其他设计工具和技术的实现。随着经验的丰富,我将会找到更多的方法来应用它并能更好的理解它所适用的时间和地点。

测试 = 规格

当您使用完 TFP 后,您所创建的测试就代表了一组可执行的规范。只要您保持测试和代码的同步(这是实践的关键点),如果有程序员想知道系统能做什么,他或她可以参看测试。

测试不是您所需的唯一的规范。让客户通过测试来判断您的规范是否正确是不合理的。作为软件工程师,我们知道存在可以用不同方式表示的许多类型的需求。认为一种类型的需求可以满足每个涉众,或者认为可以很容易地使代码和需求保持同步是很愚蠢的。然而,在此领域,TFP 真的提供了支持。

小规模工作

Mike Ciaraldi 教授,是我在伍斯特工业学院的一个同事,他说要“小规模”地(而非大规模地)衡量项目进展。我们致力于非常小的进展,使它正确,然后继续下一个进展。最终,所有的小进展汇集成大的进展。

如果想使用测试来限制进度,那么我们可以通过撰写优先测试立刻满足要求,并实现代码使之得以工作。我发现采用 TFP 可以防止出现没有内容的分析,以及由于我们不了解所有的限制和所面临的所有可能出现的问题而害怕去撰写代码的情形。

像中国的古老谚语所说的,“千里之行始于足下。” TFP 帮助我们迈出第一步,然后下一步,等等。

易于理解

TFP 是很容易理解的实践。在本学期,我用了一个小时的课对它进行介绍,提供参考资料,并带着学生完成了一个示例,例如三角形测试程序。在这一个小时的最后,学生们都走出去准备试试运气。

许多学生很快就掌握了 TFP 并报告说 TFP 改变了他们创建程序的方式。其他人不适应 TFP,我没有强迫他们采用此实践。目前,我希望他们去体验很多软件开发实践并使用适合他们风格的实践。但是,如果在他们的职业生涯中需要 TFP 时,至少他们知道如何使用。如果您是项目经理,您可能希望使用类似的方法,并花费一些时间培训您的团队使用 TFP。

多少就足够了?

大多数想要采用 TFP 的程序员会问:“多少就足够了?有必要为每行代码和每个方法书写测试吗?”

对此有一些不同的看法。我会测试我所实现的大部分方法。但是,我也不厌烦为 IDE 生成的方法书写测试。

进行测试的多少取决于项目目标和可用的时间。尽管为每个您撰写的方法创建多种测试是非常不错的,但您必须决定您实际能测试多少,结果是否能体现出您所投入的时间。

通用的实践

TFP 是实用的实践,不论您是否将 TFP 作为设计工具来使用。它提供广泛的覆盖面,是相当简单的可防止编码灾难的方法。如果认为要编写的代码很简单,我们会抄近路。但是如果您见过由于一行代码的变更而导致系统的失败,您就会理解多进行测试的必要了。

您可以在几乎任何项目环境中应用 TFP,不论组织或项目流程中是否指出要使用它。当您的队友看到您代码的质量后,他们也许会问您如何进行改进。您可以简单地分享您的“秘密”,并使之成为团队流程中的一个部分。如果没有成功地使之加入到流程中,您仍旧可以为您所撰写的测试和代码自豪。

参考资料

  • http://www.junit.org/index.htm:关于 JUnit 工程的主页提供了代码和有关如何应用 TFP 的文章。一定要阅读文档部分的文章“Test Infected -- Programmers Love Writing Tests”。
  • http://c2.com/cgi/wiki?CodeUnitTestFirst:关于 TFP 的 XP Wiki Web 页。
  • Kent Beck, Test Driven Development by Example。 Addison-Wesley,2002 年。讲述了 Beck 针对 TDD 的方法。
  • David Astels, Test-Driven Development: A Practical Guide。 Prentice Hall,2003 年。通过实例给出指导。
  • Andrew Hunt 和 David Thomas, Pragmatic Unit Testing in Java with JUnit。 The Pragmatic Programmers, LLC,2003 年。为有经验的 TFP 用户提供的指导。

单击此处,下载示例程序Triangle Tester

注释

1对 XP 不熟悉的读者可以参考http://c2.com/cgi/wiki?ExtremeProgrammingRoadmap

2参见http://c2.com/cgi/wiki?CodeUnitTestFirst

3要了解更多关于 JUnit 的信息,请访问http://www.junit.org。阅读该站点的 Test Infected 论文,获得对框架和流程的总览。同时在本文底部的参考资料中列出了一些讲述如何在 TFP 的环境下使用 JUnit 的书籍和论文。

4我们不会展示完整的程序,只展示一个实现功能的类。用 Eclipse 和 JUnit 来对该类进行测试是非常简单的。您可以通过点击文章底部的链接,下载该类的所有代码。

5 勾股定理(Pythagorean Theorem)指 32 + 42 = 9 + 16 = 25 = 52

6很明显,如果等到开发周期的晚一些的阶段再添加测试,我就会做更多的返工。这是经验之谈。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Rational
ArticleID=58305
ArticleTitle=在编码之前进行测试
publish-date=04012005