测试驱动开发(TDD) 是一种渐进的开发方法,它结合了测试优先的开发,即在编写足够的产品代码以完成测试和重构之前编写测试。TDD的主要目标是什么?一个观点是TDD的目标是规范而不是验证(Martin, Newkirk,和Kess 2003)。换句话说,这是在编写功能代码之前考虑需求或设计的一种方法(这意味着TDD是一种重要的敏捷需求和敏捷设计技术)。另一种观点认为TDD是一种编程技术。正如Ron Jeffries喜欢说的,TDD的目标是编写有效的干净代码。我认为这两种观点都是有价值的,尽管我倾向于规范的观点,但是我把它留给您来决定。
主要内容
TDD是什么?
TDD和传统测试
TDD和文档
测试驱动的数据库开发
通过敏捷模型驱动开发(AMDD)扩展TDD
为什么TDD ?
神话和误解
到底是谁在做这件事?
总结
工具
1. TDD是什么?
图1的UML活动图中概述了测试优先开发(test first development, TFD)的步骤。第一步是快速添加一个测试,基本上只需要足够的代码就可以失败。接下来运行您的测试,通常是完整的测试套件,尽管出于速度的考虑,您可能决定只运行一个子集,以确保新测试确实失败。然后更新函数代码,使其通过新的测试。第四步是再次运行测试。如果它们失败了,您需要更新您的功能代码并重新测试。一旦测试通过,下一步就是重新开始(您可能首先需要根据需要重构设计中的任何重复,将TFD转换为TDD)。
图1所示。测试优先开发(TFD)的步骤。
我喜欢用这个简单的公式来描述TDD:
TDD =重构+ TFD。
TDD彻底改变了传统开发。当您第一次实现一个新特性时,您要问的第一个问题是,现有的设计是否是使您能够实现该功能的最佳设计。如果是,则通过TFD方法进行。如果没有,则在本地重构它,以更改受新特性影响的设计部分,使您能够尽可能轻松地添加该特性。因此,您将始终提高您的设计质量,从而使它更容易在未来的工作。
与其先编写函数代码,然后再编写测试代码,如果您真的编写了测试代码,那么您应该在编写函数代码之前编写测试代码。此外,您可以通过非常小的步骤来完成—一次测试和少量对应的函数代码。采用TDD方法的程序员拒绝编写新函数,直到第一个测试失败,因为该函数不存在。事实上,在对代码进行测试之前,他们甚至拒绝添加任何一行代码。一旦测试就绪,他们就会执行确保测试套件现在通过所需的工作(您的新代码可能会破坏几个现有的测试以及新的测试)。这在原则上听起来很简单,但是当您第一次学习使用TDD方法时,它需要严格的规程,因为不首先编写新的测试,很容易“滑倒”并编写功能代码。结对编程的一个优点是,结对可以帮助您保持在正确的轨道上。
TDDSpecification by example有两个级别的TDD:
验收测试驱动开发(ATDD)。使用ATDD,您可以编写一个验收测试,或者根据您喜欢的术语编写行为规范,然后编写足够的产品功能/代码来完成该测试。ATDD的目标是在准时(JIT)的基础上为您的解决方案指定详细的、可执行的需求。ATDD也被称为行为驱动开发(BDD)。
开发人员TDD。使用开发人员TDD,您可以编写单个开发人员测试,有时不准确地称为单元测试,然后编写足够的生产代码来完成该测试。开发人员TDD的目标是在JIT的基础上为您的解决方案指定一个详细的、可执行的设计。开发人员TDD通常简单地称为TDD。
图2描述了一个UML活动图,展示了ATDD和开发人员TDD是如何结合在一起的。理想情况下,您将编写一个验收测试,然后使用开发人员TDD方法实现实现该测试所需的生产代码。这反过来要求您迭代多次,通过编写测试、编写生产代码,使其在开发人员TDD级别上工作。
图2。验收TDD和开发人员TDD如何协同工作。
请注意,图2假设您同时做这两件事,尽管可以不做任何一件事。事实上,有些团队会在不使用ATDD的情况下使用开发人员TDD,请参阅下面的调查结果,尽管如果您正在使用ATDD,那么几乎可以肯定您也在使用开发人员TDD。挑战在于,这两种形式的TDD都要求从业者具有技术测试技能,而许多需求专家通常没有这些技能(这也是为什么泛化专家比专家更可取的另一个原因)。
TDD的一个基本假设是您有一个可用的测试框架。对于可接受的TDD,人们将使用Fitnesse或RSpec等工具,对于开发人员TDD,敏捷软件开发人员通常使用xUnit系列的开源工具,如JUnit或VBUnit,尽管商业工具也是可行的选择。没有这些工具,TDD实际上是不可能的。图3展示了一个UML状态图,展示了人们通常如何使用这些工具。这个图表是Keith Ray向我提出的。
图3。通过xUnit框架进行测试。
kent Beck在eXtreme Programming (XP) (Beck 2000)中推广了TDD,他为TDD定义了两个简单的规则(Beck 2003)。首先,只有在自动化测试失败时,才应该编写新的业务代码。其次,您应该消除发现的任何重复。Beck解释了这两条简单的规则是如何产生复杂的个人和群体行为的:
您的开发是有机的,运行中的代码在决策之间提供反馈。
您编写自己的测试,因为您不能每天等待20次别人为您编写测试。
您的开发环境必须对小的变化提供快速的响应(e。你需要一个快速的编译器和回归测试套件)。
您的设计必须由高度内聚、松散耦合的组件(例如,您的设计高度规范化)组成,以使测试更容易(这也使系统的演化和维护更容易)。
对于开发人员来说,这意味着他们需要学习如何编写有效的单元测试。Beck的经验是好的单元测试:
跑得快(他们有短的设置,运行时间和故障)。
单独运行(应该能够重新排序)。
使用易于阅读和理解的数据。
在需要时使用真实数据(例如生产数据的副本)。
代表向你的总体目标迈出的一步。
2. TDD和传统测试
精益敏捷atdd tdd主要是一种规范技术,它的副作用是确保您的源代码在验证级别得到彻底的测试。然而,还有比这更多的测试。特别是在规模上,您仍然需要考虑其他敏捷测试技术,如生产前集成测试和调查性测试。如果您选择这样做(您应该这样做),那么大部分测试也可以在项目的早期完成。
使用传统的测试,成功的测试会发现一个或多个缺陷。TDD也是如此;当测试失败时,您已经取得了进展,因为您现在知道需要解决问题。更重要的是,当测试不再失败时,您可以清楚地度量成功。TDD增加了您的信心,您的系统实际上满足为它定义的需求,您的系统实际上工作,因此您可以满怀信心地继续。
与传统测试一样,系统的风险概要越大,您的测试就需要越全面。对于传统的测试和TDD,您并不追求完美,而是测试系统的重要性。套用敏捷建模(Agile Modeling, AM)的说法,您应该“有目的地进行测试”,并且知道您为什么要进行测试,以及需要测试到什么级别。TDD的一个有趣的副作用是,您可以实现100%的覆盖率测试—每一行代码都经过测试—这是传统测试无法保证的(尽管它推荐这样做)。总的来说,我认为可以相当肯定地说,尽管TDD是一种规范技术,但是它有一个有价值的副作用,那就是它比传统技术的代码测试结果要好得多。
如果值得构建,就值得测试。
如果不值得测试,为什么要浪费时间在上面呢?
3.TDD和文档
不管喜欢与否,大多数程序员都不阅读系统的书面文档,相反,他们更喜欢使用代码。这没什么不对的。当试图理解一个类或操作时,大多数程序员首先会寻找已经调用它的示例代码。编写良好的单元测试正是这样做的——提供功能代码的工作规范——因此单元测试有效地成为技术文档的重要部分。这意味着支持文档的人群的期望需要反映这一现实。类似地,验收测试可以成为需求文档的重要部分。当你停下来思考的时候,这是很有意义的。您的验收测试准确地定义了涉众对您的系统的期望,因此它们指定了您的关键需求。您的回归测试套件,特别是使用测试优先的方法,有效地成为详细的可执行规范。
测试是否有足够的文档?很可能不会,但它们确实构成了其中重要的一部分。例如,您可能会发现仍然需要用户、系统概述、操作和支持文档。您甚至可能发现,您需要摘要文档来查看系统支持的业务流程。当您以开放的心态处理文档时,我怀疑您会发现这两种类型的测试涵盖了开发人员和业务涉众对文档的大部分需求。此外,它们是AM的单一源信息实践的一个很好的例子,也是您在文档方面保持尽可能敏捷的整体努力的一个重要部分。
4. 测试驱动的数据库开发
在撰写本文时,敏捷社区中提出的一个重要问题是“TDD可以用于面向数据的开发吗?”“当您查看图1中描述的流程时,需要注意的是没有一个步骤指定对象编程语言,比如Java或c#,即使这些是通常使用TDD的环境。为什么不能在更改数据库模式之前编写测试?为什么不能根据需要进行更改、运行测试和重构模式呢?在我看来,你只需要选择这样做。
我的猜测是,在短期内,数据库TDD,或者测试驱动数据库设计(TDDD),将不会像应用程序TDD那样工作得那么顺利。第一个挑战是工具支持。尽管诸如DBUnit之类的单元测试工具现在已经可用,但在撰写本文时,它们仍然是一种新兴的技术。一些dba正在改进他们所做的测试的质量,但是我还没有看到任何人采用TDD方法进行数据库开发。一个挑战是单元测试工具在数据社区中仍然没有被很好地接受,尽管这正在发生变化,所以我的预期是在未来几年数据库TDD将会增长。其次,对许多数据专业人员来说,演进开发的概念是新的,因此采用TDD方法的动机尚未形成。这个问题影响了数据专业人员可用的工具的性质——因为在传统的数据社区中,串行思维仍然占主导地位,大多数工具不支持渐进开发。我希望工具供应商能够跟上这种范式的转变,但是我的期望是我们需要开发开源工具。第三,我的经验是,大多数从事数据导向工作的人似乎更喜欢模型驱动的方法,而不是测试驱动的方法。其中一个原因可能是因为测试驱动方法直到现在才被广泛考虑,另一个原因可能是许多数据专业人员可能是视觉思考者,因此更喜欢模型驱动方法。
5. 通过敏捷模型驱动开发(AMDD)扩展TDD
TDD非常擅长于详细的规范和验证,但不擅长考虑更大的问题,比如总体设计、人们将如何使用系统或UI设计(例如)。建模,或者更接近于敏捷模型驱动开发(AMDD)(图4中捕捉到的生命周期)更适合于此。AMDD解决了TDD没有解决的敏捷扩展问题。
图4。敏捷模型驱动开发(AMDD)生命周期。
比较TDD和AMDD:
TDD缩短了编程反馈循环,AMDD缩短了建模反馈循环。
TDD提供了详细的规范(测试),而AMDD更适合考虑更大的问题。
TDD促进高质量代码的开发,而AMDD促进与涉众和其他开发人员的高质量通信。
TDD提供了软件工作的具体证据,而AMDD支持您的团队(包括涉众)朝着共同的理解努力。
TDD与程序员“交谈”,而AMDD与业务分析师、涉众和数据专业人员交谈。
TDD提供了非常细粒度的关于分钟顺序的具体反馈,而AMDD支持关于分钟顺序的口头反馈(具体的反馈要求开发人员按照实践用代码证明它,因此依赖于非am技术)。
通过关注可调用和可测试操作的创建,TDD有助于确保您的设计是干净的,而AMDD提供了在编写代码之前考虑更大的设计/体系结构问题的机会。
TDD是非面向视觉的,而AMDD是面向视觉的。
这两种技术对传统开发人员来说都是新技术,因此可能对他们构成威胁。
这两种技术都支持进化开发。
你应该采取哪种方法?答案取决于你和你的队友的认知偏好。有些人主要是“视觉思考者”,也被称为空间思考者,他们可能更喜欢通过绘画来思考问题。其他人主要是面向文本、非视觉或非空间的思考者,他们不能很好地处理绘图,因此他们可能更喜欢TDD方法。当然,大多数人都处于这两个极端的中间,因此他们更喜欢在最有意义的时候使用每种技术。简而言之,答案是将这两种技术结合起来使用,从而获得两者的优势。
如何将这两种方法结合起来?应该使用AMDD与项目涉众一起创建模型,以帮助研究他们的需求,然后在体系结构和设计模型(通常是简单的草图)中充分地研究这些需求。TDD应该作为构建工作的关键部分来使用,以确保开发干净的、可工作的代码。最终的结果是,您将拥有一个高质量的、能够满足项目涉众实际需求的工作系统。
6. 为什么TDD ?
TDD的一个显著优势是,它允许您在编写软件时采取一些小步骤。这是我多年来一直提倡的一种实践,因为它比尝试以大步骤编写代码的效率高得多。例如,假设您添加了一些新的函数代码,编译并测试它。您的测试很可能会被新代码中存在的缺陷破坏。如果您已经编写了两行代码,那么查找并修复这些缺陷要比编写两千行代码容易得多。这意味着,编译器和回归测试套件运行得越快,就越有吸引力进行越来越小的步骤。在重新编译和重新运行测试之前,我通常更喜欢添加几行新的函数代码,通常少于10行。
我认为Bob Martin说得很好:“编写单元测试的行为更多的是一种设计行为,而不是验证行为。它也更多的是一种文件化的行为,而不是验证的行为。编写单元测试的行为关闭了大量的反馈循环,其中最少的是与功能验证相关的循环。
许多人对敏捷技术的第一反应是,对于小型项目,比如几个月来只涉及少数人的项目,他们是可以接受的,但是对于更大的“真正的”项目,他们就不会工作了。这根本不是真的。Beck(2003)报告了在Smalltalk系统上使用完全测试驱动的方法,该方法花费了4年和40个人的时间,产生了25万行功能代码和25万行测试代码。在20分钟内运行4000个测试,整个套件每天运行几次。虽然有更大的系统,但我个人曾在涉及几百年工作经验的系统中工作过,很明显TDD适用于大型系统。
7. 神话和误解
关于TDD,有几个常见的神话和误解,如果可能的话,我想澄清一下。表1列出了这些神话并描述了现实。
表1。解决围绕TDD的神话和误解。
神话现实您创建了一个100%回归测试套件虽然这听起来是个不错的目标,但不幸的是,这并不现实,原因如下:
我可能有一些可重用的组件/框架/…我下载或购买的软件没有附带测试套件,甚至可能没有源代码。虽然我可以创建黑盒测试来验证组件的接口,但这些测试不会完全验证组件。
用户界面真的很难测试。尽管用户界面测试工具确实存在,但并不是每个人都拥有它们,有时它们很难使用。一个常见的策略是不自动化用户界面测试,而是希望用户测试工作涵盖系统的这个重要方面。这不是一种理想的方法,但仍然是一种常见的方法。
团队中的一些开发人员可能没有足够的测试技能。
数据库回归测试是一个相当新的概念,还没有得到工具的很好支持。
我可能正在处理遗留系统,可能还没有抽出时间为一些遗留功能编写测试。
单元测试构成您的设计规范的100%刚接触敏捷软件开发的人,或者自称敏捷但实际上并不敏捷的人,或者可能从未参与过实际敏捷项目的人,有时会这么说。实际情况是,单元测试形成了设计规范的一部分,类似的验收测试形成了需求规范的一部分,但是还有更多。正如图4所示,敏捷者实际上会建模(并为此编写文档),只是我们在如何做这件事上非常聪明。因为在编写产品代码之前要考虑生产代码,所以可以有效地执行详细设计,因为我强烈建议阅读我的单一源代码信息:有效文档的敏捷实践文章。您只需要进行单元测试对于除最简单的系统外的所有系统,这都是完全错误的。敏捷社区非常清楚需要大量其他测试技术。TDD足以进行测试TDD,在单元/开发人员测试以及客户测试级别,只是您整体测试工作的一部分。它最多包含您的验证性测试工作,但是如图5所示,您还必须关注超出此范围的独立测试工作。有关敏捷测试策略的详细信息,请参阅敏捷测试和质量策略:事实胜于雄辩。TDD不能扩展这在一定程度上是正确的,尽管很容易克服。TDD可伸缩性问题包括:
您的测试套件运行时间太长。这是一个具有相同解决方案的常见问题。首先,将您的测试套件分成两个或更多的组件。一个测试套件包含您当前正在处理的新功能的测试,另一个测试套件包含所有测试。您将定期运行第一个测试套件,并将针对生产代码的成熟部分的旧测试迁移到整个测试套件中。整个测试套件在后台运行,通常在单独的机器上运行,并且/或者在晚上运行。在scale上,我看到了几个级别的测试套件——开发沙箱测试在5分钟或更短的时间内运行,项目集成测试在几个小时或更短的时间内运行,测试套件在很多小时甚至几天内运行,但运行的频率更低。在一个项目中,我看到了一个运行了几个月的测试套件(重点是负载/压力测试和可用性)。其次,在这个问题上投入一些硬件。
并不是所有的开发人员都知道如何测试。这通常是正确的,所以让他们接受一些适当的培训,并让他们与具有单元测试技能的人合作。任何经常抱怨这个问题的人似乎都在寻找不采用TDD的借口。
并非每个人都采用TDD方法。采用TDD方法进行开发是团队中的每个人都需要同意做的事情。如果有些人不这样做,那么按照优先顺序:他们需要开始,他们需要离开团队,或者您的团队应该放弃TDD。
图5。敏捷项目团队测试概述。
8. 到底是谁在做这件事?
不幸的是,TDD的采用率没有我希望的那么高。图6总结了2010年的结果:您有多敏捷?提供了关于声称敏捷的团队正在遵循哪些验证策略的洞察。我怀疑开发人员TDD和验收TDD的采用率(分别为53%和44%)比我在2008年测试驱动开发(Test Driven Development, TDD)调查中报告的采用率要实际得多。
图6。敏捷团队如何验证他们自己的工作。
9. 总结
测试驱动开发(TDD)是一种开发技术,在编写新的功能代码之前,必须先编写一个失败的测试。TDD正在被敏捷软件开发人员迅速采用,用于开发应用程序源代码,甚至被敏捷dba用于数据库开发。TDD应该被看作是敏捷模型驱动开发(AMDD)方法的补充,并且这两者可以并且应该一起使用。TDD并没有取代传统的测试,相反,它定义了一种经过验证的方法来确保有效的单元测试。TDD的一个副作用是,生成的测试是用于调用代码的工作示例,从而为代码提供了一个工作规范。我的经验是,TDD在实践中工作得非常好,所有软件开发人员都应该考虑采用TDD。
10. 工具
下面是您可以使用的TDD工具的代表性列表。请给我发电子邮件提出建议。我还维护了一个敏捷数据库开发工具列表。
cpputest
csUnit (.Net)
CUnit
DUnit (Delphi)
DBFit
DBUnit
DocTest (Python)
Googletest
HTMLUnit
HTTPUnit
JMock
JUnit
Moq
NDbUnit
NUnit
OUnit
PHPUnit
PyUnit (Python)
SimpleTest
TestNG
TestOoB (Python)
Test::Unit (Ruby)
VBUnit
XTUnit
xUnit.net
.Net developers may find this comparison of .Net TDD tools interesting.