单元测试能否成功添加到现有生产项目中?如果是这样,它是如何值得的?

时间:2010-08-13 10:39:33

标签: unit-testing testing tdd

我强烈考虑将单元测试添加到正在生产的现有项目中。它是在18个月前开始的,之后我才能真正看到TDD (面掌) 的任何好处,所以现在它是一个相当大的解决方案,有很多项目我没有'最模糊的想法从哪里开始添加单元测试。是什么让我觉得这是偶尔出现一个旧的bug似乎重新出现,或者在没有真正修复的情况下将错误检查为已修复。单元测试可以减少或防止出现这些问题。

通过阅读关于SO的 similar 问题,我看到了诸如从错误跟踪器开始并为每个错误编写测试用例以防止回归的建议。但是,我担心的是,如果我从开始使用TDD,我最终会错过大局并最终错过了基本测试。

是否有任何流程/步骤需要遵守,以确保现有解决方案正确单元测试而不仅仅是提交?我如何确保测试质量良好,而不仅仅是一个案例任何测试都优于没有测试

所以我猜我也想问的是;

  • 值得付出努力 现有的生产解决方案?
  • 最好忽略测试 对于这个项目并将其添加到 可能的未来重写?
  • 什么会更有益;开支 几个星期加入测试或一些 几周添加功能?

(显然,第三点的答案完全取决于您是否与管理层或开发人员交谈)


赏金原因

添加赏金以尝试吸引更广泛的答案,这不仅证实了我现在的怀疑,这是一件好事,而且还有一些很好的理由。

我的目标是稍后用优点和缺点写出这个问题,试图向管理层表明,将产品的未来发展转移到TDD是值得的。我想接受这个挑战并在没有偏见的情况下发展我的推理。

23 个答案:

答案 0 :(得分:161)

答案 1 :(得分:23)

  
      
  • 现有的生产解决方案是否值得努力?
  •   

是!

  
      
  • 最好忽略对该项目的测试,并将其添加到将来可能的重写中吗?
  •   

没有!

  
      
  • 什么会更有益;花几周时间添加测试或几周添加功能?
  •   

添加测试(尤其是自动化测试)使得 更容易让项目在未来保持正常运行,并且它使您运输愚蠢的可能性大大降低对用户的问题。

放入先验的测试是检查您认为代码的公共接口(以及其中的每个模块)是否按照您的想法运行的测试。如果可以的话,也尝试引入代码模块应该具有的每个隔离故障模式(请注意,这可能非常重要,您应该小心不要过于谨慎地检查事情是如何失败的,例如,您并不是真的想要做一些事情,比如计算失败时产生的日志消息的数量,因为验证它已被记录就足够了。

然后对您的错误数据库中的每个当前错误进行测试,该错误导致错误,并在错误修复时通过。然后修复那些错误! : - )

添加测试需要花费很多时间,但是后端会因为代码的质量要高得多而得到多次回报。当您尝试发布新版本或进行维护时,这非常重要。

答案 2 :(得分:15)

改装单元测试的问题是你会意识到你没有考虑在这里注入依赖或在那里使用接口,不久之后你将重写整个组件。如果你有时间这样做,你将建立一个很好的安全网,但你可能会引入一些微妙的错误。

我参与过许多项目,从第一天起就真正需要进行单元测试,并且没有简单的方法可以将它们放在那里,缺少完整的重写,当代码工作时已经不合理了赚钱。最近,我已经编写了powershell脚本,这些脚本以一种方式运行代码,一旦它被引发就会再现一个缺陷,然后将这些脚本保存为一系列回归测试,以便进行进一步的更改。这样你至少可以开始为应用程序构建一些测试而不会过多地改变它,但是,这些更像是端到端的回归测试而不是正确的单元测试。

答案 3 :(得分:11)

我同意大多数人所说的话。向现有代码添加测试很有价值。我永远不会不同意这一点,但我想补充一点。

尽管在现有代码中添加测试很有价值,但确实需要付出代价。它的代价是构建新功能。这两个方面如何平衡完全取决于项目,并且有许多变量。

  • 将所有代码置于测试中需要多长时间?天?周?月?年?
  • 你是为谁编写这段代码的?支付客户?一位教授?一个开源项目?
  • 你的日程安排是什么样的?你有必须遇到的艰难的最后期限吗?你有任何截止日期吗?

再一次,让我强调一下,测试很有价值,你应该努力测试你的旧代码。这实际上更多的是你如何处理它。如果您能够放弃所有内容并将所有旧代码置于测试之下,那就去做吧。如果这不现实,那么你应该做什么至少

  • 您编写的任何新代码都应完全在单元测试下
  • 您碰巧碰到的任何旧代码(错误修复,扩展程序等)都应置于单元测试
  • 之下

此外,这不是一个全有或全无的主张。如果您有一个由四人组成的团队,并且您可以通过将一两个人放在遗留测试职责上来满足您的最后期限,那么一定要这样做。

修改

  

我的目标是稍后用优点和缺点写出这个问题,试图向管理层表明,值得花费时间将产品的未来发展转移到TDD。

这就像问“使用源代码管理有什么优缺点?”或“在雇用人员之前采访人们有什么利弊?”或“呼吸的利弊是什么?”

有时争论只有一方。你需要为任何复杂的项目进行某种形式的自动化测试。不,测试不会自己写,而且,是的,需要一些额外的时间才能把事情搞得一团糟。但从长远来看,事先编写测试需要花费更多的时间和成本来修复错误。期间。这就是它的全部内容。

答案 4 :(得分:8)

当我们开始添加测试时,它是一个已有十年历史的百万行代码库,在用户界面和报告代码中有太多的逻辑。

我们做的第一件事(在建立连续构建服务器之后)是添加回归测试。这些是端到端的测试。

  • 每个测试套件首先将数据库初始化为已知状态。我们实际上有几十个我们保留在Subversion中的回归数据集(由于其庞大的规模,在我们的代码的单独存储库中)。每个测试的FixtureSetUp将其中一个回归数据集复制到临时数据库中,然后从那里运行。
  • 测试夹具设置然后运行一些我们感兴趣的结果的过程。(这一步是可选的 - 一些回归测试仅用于测试报告。)
  • 然后,每个测试运行一个报告,将报告输出到.csv文件,并将该.csv的内容与保存的快照进行比较。这些快照.csvs存储在每个回归数据集旁边的Subversion中。如果报告输出与保存的快照不匹配,则测试失败。

回归测试的目的是告诉你某些事情是否有所改变。这意味着如果你破坏了它们就会失败,但是如果你故意改变了某些东西它们也会失败(在这种情况下,修复是更新快照文件)。您不知道快照文件是否正确 - 系统中可能存在错误(然后当您修复这些错误时,回归测试将失败)。

然而,回归测试对我们来说是一个巨大的胜利。我们系统中的所有内容都有一份报告,所以通过花费几周的时间来获得报告的测试工具,我们能够在代码库的大部分内容中获得一定程度的覆盖。编写等效单元测试可能需要数月或数年。 (单元测试会给我们提供更好的覆盖范围,而且本来就不那么脆弱;但我现在宁愿拥有一些东西,而不是等待多年的完美。)

然后,当我们修复错误,添加增强功能或需要理解某些代码时,我们回去开始添加单元测试。回归测试绝不会消除单元测试的需要;它们只是一级安全网,因此您可以快速获得一些级别的测试覆盖率。然后你可以开始重构来破坏依赖关系,所以你可以添加单元测试;并且回归测试可以让您确信您的重构不会破坏任何内容。

回归测试存在问题:它们很慢,并且有太多原因导致它们破裂。但至少对我们来说,他们所以值得。他们在过去的五年里遇到了无数的漏洞,他们在几个小时内就赶上了,而不是等待QA周期。我们仍然有那些原始的回归测试,分布在七个不同的连续构建机器上(与运行快速单元测试的机器分开),我们甚至不时添加它们,因为我们仍然有如此多的代码,我们的6000 +单元测试不包括在内。

答案 5 :(得分:7)

绝对值得。我们的应用程序具有复杂的交叉验证规则,最近我们不得不对业务规则进行重大更改。我们最终遇到了阻止用户保存的冲突。我意识到在应用程序中对它进行整理需要花费很长时间(只需几分钟就可以解决问题)。我想引入自动化单元测试并安装了框架,但除了几个虚拟测试之外我还没有做任何事情以确保工作正常。随着新业务规则的出台,我开始编写测试。测试很快确定了导致冲突的条件,我们能够澄清规则。

如果您编写涵盖正在添加或修改的功能的测试,您将立即获益。如果您等待重写,您可能永远不会进行自动化测试。

你不应该花很多时间为已经有效的现有东西编写测试。大多数情况下,您没有现有代码的规范,因此您正在测试的主要是您的逆向工程能力。另一方面,如果您要修改某些内容,则需要通过测试覆盖该功能,以便您知道自己正确地进行了更改。当然,对于新功能,编写失败的测试,然后实现缺少的功能。

答案 6 :(得分:5)

我会加上我的声音并说是的,它总是有用的!

但是你应该记住一些区别:黑盒子与白盒子,单位与功能。由于定义各不相同,这就是我的意思:

  • 黑盒 =在没有实施专业知识的情况下编写的测试,通常会在边缘情况下进行调查,以确保事情像天真的用户所期望的那样发生。
  • 白盒 =使用实施知识编写的测试,通常会尝试使用众所周知的失败点。
  • 单元测试 =单个单元(功能,可分离模块等)的测试。例如:确保数组类按预期工作,并且字符串比较函数返回各种输入的预期结果。
  • 功能测试 =同时测试整个系统。这些测试将同时运行系统的很大一部分。例如:init,打开连接,做一些真实的东西,关闭,终止。我喜欢区分这些和单元测试,因为它们有不同的用途。

当我在游戏后期添加测试时,我发现我从白盒功能测试中获得了最大的收益。如果您知道的代码的任何部分特别脆弱,请编写白盒测试以涵盖问题案例,以帮助确保它不会以相同的方式破解两次。同样,整个系统的功能测试是一个有用的健全性检查,可以帮助您确保永远不会破坏10个最常见的用例。

小单位的黑盒和单元测试也很有用,但如果时间有限,最好尽早添加它们。当您发货时,您通常会发现(艰难的)大多数边缘情况和这些测试会发现的问题。

与其他人一样,我也会提醒您关于TDD的两个最重要的事情:

  1. 创建测试是一项持续的工作。它永远不会停止。每次编写新代码或修改现有代码时,都应尝试添加新测试。
  2. 您的测试套件绝对不可靠!不要让您进行测试的事实会让您陷入虚假的安全感。仅仅因为它通过测试套件并不意味着它正常工作,或者你没有引入微妙的性能回归等。

答案 7 :(得分:4)

是否值得将单元测试添加到正在生产的应用程序中取决于维护应用程序的成本。如果应用程序有很少的错误和增强请求,那么也许它不值得努力。 OTOH,如果应用程序有缺陷或经常修改,那么单元测试将非常有用。

此时,请记住我正在谈论有选择地添加单元测试,而不是尝试生成一组类似于从一开始就练习TDD时存在的测试。因此,在回答第二个问题的后半部分时:在下一个项目中使用TDD,无论是新项目还是重写(道歉,但这里是另一个你应该阅读的书:Growing Object Oriented Software Guided by Tests

我对第三个问题的回答与第一个问题相同:它取决于项目的背景。

嵌入您的内容是另一个问题,即确保任何改装的测试 正确 。要确保的重要一点是,单元测试确实是 单元 测试,而这(通常情况下)通常意味着改造测试需要重构现有代码以允许层分离/ components(参见依赖注入;控制反转;存根;模拟)。如果你没有强制执行这个,那么你的测试就会变成集成测试,这些测试很有用,但是比真正的单元测试更有针对性和更脆弱。

答案 8 :(得分:4)

您没有提及实现语言,但如果是Java,那么您可以尝试这种方法:

  1. 在单独的源代码树构建回归或“冒烟”测试中,使用工具生成它们,这可能会使您接近80%的覆盖率。这些测试执行所有代码逻辑路径,并从那一点验证代码仍然完全正如它当前所做的那样(即使存在错误)。这为您提供了一个安全网,可防止在进行必要的重构时无意中改变行为,从而使代码易于手动进行单元测试。

  2. 对于您修复的每个错误或从现在开始添加的功能,请使用TDD方法确保新代码可以测试并将这些测试放在正常的测试源树中。

  3. 现有代码也可能需要更改或重构,以便在添加新功能时使其可测试;您的烟雾测试将为您提供一个安全网,以防止回归或无意中微妙地改变行为。

  4. 通过TDD进行更改(错误修复或功能)时,如果完成,则伴随冒烟测试可能会失败。由于所做的更改,验证故障是否符合预期,并删除不太可读的烟雾测试,因为您的手写单元测试完全覆盖了该改进的组件。确保您的测试覆盖率不会下降,只保持不变或增加。

  5. 修复错误时,请编写一个首先暴露错误的失败单元测试。

答案 9 :(得分:3)

我想通过说单元测试非常重要来开始这个答案,因为它可以帮助你在蠕虫进入生产之前阻止它们。

确定重新引入错误的项目/模块区域。从那些编写测试的项目开始。为新功能和错误修复编写测试是完全有意义的。

  

现有的努力是否值得   生产中的解决方案?

是。您将看到错误的影响,维护变得更容易

  

忽略测试会更好吗?   对于这个项目并将其添加到   可能的未来重写?

我建议从现在开始。

  

什么会更有益;开支   几个星期加入测试或一些   几周添加功能?

你问的是错误的问题。当然,功能比其他任何东西都重要。但是,你应该问一下,花几周时间增加测试会让我的系统更稳定。这有助于我的最终用户吗?它是否有助于团队中的新开发人员理解项目,并确保他/她不会因缺乏对变更整体影响的理解而引入错误。

答案 10 :(得分:3)

我非常喜欢Refactor the Low-hanging Fruit作为解决从哪里开始重构的问题的答案。这是一种轻松进入更好设计的方法,而不会比你咀嚼更多。

我认为相同的逻辑适用于TDD - 或者只是单元测试:根据需要编写您需要的测试;为新代码编写测试;在出现错误时编写测试。你担心忽略代码库中难以触及的区域,这肯定是一种风险,但作为一种入门方式:开始吧!您可以通过代码覆盖工具降低风险,并且风险不是(在我看来)那么大,无论如何:如果您正在覆盖错误,覆盖新代码,覆盖您正在查看的代码,那么你将覆盖最需要测试的代码。

答案 11 :(得分:2)

  • 是的,确实如此。当您开始添加新功能时,它可能会导致一些旧的代码修改,因此它会导致潜在的错误。
  • (请参阅第一篇)在开始添加新功能之前,所有(或几乎)代码(理想情况下)应该由单元测试覆盖。
  • (见第一个和第二个):)。一个新的宏伟功能可以“破坏”旧的工作代码。

答案 12 :(得分:2)

<强>更新

在原始答案之后6年,我的观点略有不同。

我认为将单元测试添加到您编写的所有新代码中是有意义的 - 然后重构您进行更改的位置以使其可测试。

为所有现有代码一次编写测试将无济于事 - 但是不编写您编写的新代码(或您修改的区域)的测试也没有意义。在重构/添加内容时添加测试可能是添加测试并使代码在没有测试的现有项目中更易维护的最佳方式。

早期回答

我会在这里引起一些人的注意:)

首先,你的项目是什么 - 如果它是一个编译器或语言或框架或其他任何不会在功能上长时间发生变化的东西,那么我认为添加单元测试绝对是太棒了。

但是,如果您正在处理可能需要更改功能的应用程序(因为需求更改),那么就没有必要花费额外的精力。

为什么?

  1. 单元测试仅涵盖代码测试 - 代码是否符合其设计目标 - 它不能替代手动测试,无论如何必须完成(发现功能性错误,可用性问题和所有其他类型的问题)

  2. 单位测试成本时间!现在我来自哪里,这是一种宝贵的商品 - 而且企业通常会在完整的测试套件中选择更好的功能。

  3. 如果您的应用程序对用户来说甚至是远程有用的,那么他们将要求更改 - 因此您将拥有能够更好,更快并且可能做新事物的版本 - 可能还会有很多重构你的代码增长了。在动态环境中维护完整的单元测试套件令人头疼。

  4. 单元测试不会影响产品的感知质量 - 用户看到的质量。当然,您的方法可能与第1天完全一样,表示层和业务层之间的界面可能是原始的 - 但猜猜是什么?用户不在乎!找一些真正的测试人员来测试你的应用程序。通常情况下,这些方法和界面迟早会发生变化。

  5. 什么会更有益;花几周时间添加测试或几周添加功能? - 除了编写测试之外,还有很多东西可以做得更好 - 编写新功能,提高性能,提高可用性,编写更好的帮助手册,解决待处理的错误等等。

    现在不要误会我的意思 - 如果你对未来100年的事情不会发生变化表示绝对肯定,请继续前进,自己敲门并编写测试。自动化测试也是API的一个好主意,您绝对不想破坏第三方代码。在其他任何地方,它只是让我以后出货的东西!

答案 13 :(得分:2)

对于现有的生产解决方案,是否值得努力?
是。但是您不必编写所有单元测试即可开始使用。只需逐个添加它们。

是否最好忽略对该项目的测试并将其添加到未来可能的重写中?
不会。第一次添加破坏功能的代码时,您会后悔的。

什么会更有益;花几周时间添加测试或几周添加功能?
对于新功能(代码),它很简单。您先编写单元测试然后再编写功能。 对于旧代码,您决定在路上。您不必进行所有单元测试...添加最不受伤害的单元测试...时间(和错误)将告诉您必须关注哪一个;)

答案 14 :(得分:2)

是的,它可以:尝试确保您现在编写的所有代码都已进行测试。

如果已经存在的代码需要修改并且可以进行测试,那么这样做,但最好不要过于频繁地尝试为稳定的代码进行测试。这种事情往往会产生连锁反应,可能会失控。

答案 15 :(得分:1)

你说你不想买另一本书。所以请阅读Michael Feather关于working effectively with legacy code的文章。然后买书:)

答案 16 :(得分:1)

您不太可能拥有重要的测试覆盖率,因此您必须对添加测试的位置采取策略:

  • 正如您所提到的,当您发现错误时,现在是编写测试(重现它),然后修复错误的好时机。如果您看到测试重现该错误,您可以确定它是一个很好的,alid测试。鉴于如此大部分的错误都是回归(50%?),几乎总是值得编写回归测试。
  • 当您深入到代码区域进行修改时,现在是围绕它编写测试的好时机。根据代码的性质,不同的测试是合适的。一套好的建议is found here
OTOH,不值得坐在人们满意的代码周围编写测试 - 特别是如果没有人要修改它。它只是没有增加价值(除了可能理解系统的行为)。

祝你好运!

答案 17 :(得分:1)

如果我在你的位置,我可能采取从外到内的方法,从运行整个系统的功能测试开始。我会尝试使用像RSpec这样的BDD规范语言重新记录系统的要求,然后编写测试以通过自动化用户界面来验证这些要求。

然后我会针对新发现的错误进行缺陷驱动开发,编写单元测试以重现问题,并在测试通过之前处理错误。

对于新功能,我会坚持使用从外到内的方法:从RSpec中记录的功能开始,并通过自动化用户界面(当然最初会失败)进行验证,然后添加更细粒度的单元测试作为实现继续前进。

我不是这个过程的专家,但是从我的经验来看,我可以告诉你BDD通过自动化的UI测试并不容易,但我认为值得付出努力,并且可能会给你带来最大的好处。情况下。

答案 18 :(得分:1)

我不是一个经验丰富的TDD专家,但我当然会说尽可能多地进行单元测试非常重要。由于代码已经到位,我首先要实现某种单元测试自动化。我使用TeamCity来执行我的项目中的所有测试,并且它为您提供了组件如何做的很好的总结。

有了这些,我就会转向那些真正关键的业务逻辑组件,这些组件不会失败。在我的情况下,有一些基本的三角函数问题需要解决各种输入,所以我测试了那些。我这样做的原因是,当我正在燃烧午夜油时,很容易浪费时间深入挖掘真正不需要触及的代码深度,因为你知道它们已经过测试对于所有可能的输入(在我的情况下,输入数量有限)。

好的,所以现在你希望对这些关键部分感觉更好。而不是坐下来敲打所有的测试,我会在它们出现时攻击它们。如果您遇到了一个真正需要修复的PITA的错误,请为它编写单元测试并将它们排除在外。

在某些情况下,您会发现测试很难,因为您无法从测试中实例化特定的类,因此您必须模拟它。哦,但也许你不能轻易地嘲笑它,因为你没有写入界面。我把这些“哎呀”场景作为实现所述界面的机会,因为,这是一件好事。

从那里,我将获得您的构建服务器或您使用代码覆盖工具配置的任何自动化。它们会创建令人讨厌的条形图,其中包含较大的红色区域,而您的覆盖范现在100%的覆盖率不是你的目标,100%的覆盖率也不一定意味着你的代码是防弹的,但是当我有空闲时间时,红色条形肯定会激励我。 :)

答案 19 :(得分:1)

有很多好的答案,所以我不会重复他们的内容。我检查了你的个人资料,看来你是C#.NET开发人员。因此,我正在添加对Microsoft PEX and Moles项目的引用,该项目可以帮助您自动生成遗留代码的单元测试。我知道自动生成不是最好的方法,但至少它是开始的方式。查看MSDN杂志上关于使用PEX for legacy code的这篇非常有趣的文章。

答案 20 :(得分:1)

我建议由TopTal工程师阅读一篇精彩的article,解释在哪里开始添加测试:它包含大量数学,但基本思路是:

1)测量代码的传入耦合(CA)(其他类使用了多少类,意味着破坏它会造成广泛的破坏)

2)测量代码的 Cyclomatic Complexity(CC)(更高的复杂性=更高的破坏变化)

  

您需要识别具有高CA和CC的类,即具有 f(CA,CC)的函数,并且两个度量之间具有最小差异的类应该被赋予测试覆盖率的最高优先级。

为什么呢?因为高CA但非常低的CC类非常重要但不太可能破坏。另一方面,低CA但高CC可能会破坏,但会造成较小的损害。所以你想要平衡。

答案 21 :(得分:0)

这取决于...... 进行单元测试非常棒,但您需要考虑用户是谁以及他们愿意容忍的内容,以便获得更多无错误的产品。不可避免的是,通过重构目前没有单元测试的代码,你会引入错误,很多用户会发现很难理解你是否会使产品暂时更具缺陷,从长远来看它会减少缺陷。最终,用户将拥有最后的发言权......

答案 22 :(得分:-2)

是。 没有。 添加测试。

采用更加TDD的方法实际上会更好地为您添加新功能并使回归测试变得更加容易。看看吧!