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

djd*_*d87 132 testing tdd unit-testing

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

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

是否有任何应遵循的流程/步骤,以确保现有解决方案经过适当的单元测试而不仅仅是提交?如何确保测试质量良好,并且不仅仅是测试的情况比没有测试更好.

所以我想我也要问的是;

  • 对于现有的生产解决方案,是否值得努力?
  • 是否最好忽略对该项目的测试并将其添加到未来可能的重写中?
  • 什么会更有益; 花几周时间添加测试或几周添加功能?

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


赏金的理由

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

我的目标是稍后用优点和缺点写出这个问题,试图向管理层表明,值得花费时间将产品的未来发展转移到TDD上.我希望在没有自己偏见的观点的情况下接近这一挑战并发展我的推理.

Mah*_*l25 168

我已经将单元测试引入了以前没有它的代码库.我参与的最后一个大项目我做到了这一点,当我到达团队时,产品已经投入生产,零单元测试.当我离开 - 2年后 - 我们进行了4500多次测试,在代码库中产生了大约33%的代码覆盖率,其中包含23万+生产LOC(实时财务Win-Forms应用程序).这可能听起来很低,但结果却是代码质量和缺陷率的显着提高 - 以及提高的士气和盈利能力.

如果您对相关各方有准确的理解和承诺,就可以完成.

首先,重要的是要了解单元测试本身就是一项技能.按照"常规"标准,您可以成为一个非常高效的程序员,并且仍然很难以在更大的项目中扩展的方式编写单元测试.

此外,特别针对您的情况,将单元测试添加到没有测试的现有代码库本身也是一项专业技能.除非您或您团队中的某个人有成功将单元测试引入现有代码库的经验,否则我会说阅读Feather的书是一项要求(不是可选的或强烈推荐的).

过渡到单元测试代码是对人员和技能的投资,就像代码库的质量一样.理解这一点在心态和管理期望方面非常重要.

现在,您的意见和问题:

但是,我担心我最终会错过大局并最终错过了如果我从开始使用TDD就会包含的基本测试.

简短的回答:是的,你会错过测试,是的,他们最初可能看起来不像他们在绿地情况下会有的.

更深层次的答案是这样的:没关系.你从没有测试开始.开始添加测试,并随时重构.随着技能水平的提高,开始提高添加到项目中的所有新编写代码的标准.不断改进......

现在,在这两行之间进行阅读,我得到的印象是,这是"完美作为不采取行动的借口"的思维方式.更好的心态是专注于自信.因此,您可能还不知道该怎么做,您将弄清楚如何去填充空白.因此,没有理由担心.

再一次,它是一项技能.你不能以线性方式在一个"过程"或"一步一步"的烹饪书方法中从零测试到TDD完美.这将是一个过程.您的期望必须是逐步和渐进的进步和改进.没有神奇药丸.

好消息是,随着几个月(甚至几年)的过去,您的代码将逐渐开始成为"正确的"良好的因素和经过良好测试的代码.

作为旁注.您会发现在旧的代码库中引入单元测试的主要障碍是缺乏内聚和过多的依赖性.因此,您可能会发现最重要的技能将成为如何打破现有依赖关系和解耦代码,而不是自己编写实际的单元测试.

是否有任何应遵循的流程/步骤,以确保现有解决方案经过适当的单元测试而不仅仅是提交?

除非您已经拥有它,否则请设置构建服务器并设置一个持续集成构建,该构建在每个签入时运行,包括所有具有代码覆盖率的单元测试.

培训你的员工.

从客户的角度出发,从某个地方开始并开始添加测试(见下文).

使用代码覆盖率作为您生产代码库的测试数量的指导参考.

构建时间应始终是快速的.如果您的构建时间很慢,那么您的单元测试技能就会滞后.找到慢速测试并改进它们(分离生产代码并单独测试).写得不错,你应该能够轻松地进行数千次单元测试,并且仍然可以在10分钟内完成构建(~1-ms ms /测试是一个很好但非常粗略的准则,一些例外可能适用于使用反射等的代码).

检查和适应.

如何确保测试质量良好,并且不仅仅是测试的情况比没有测试更好.

你自己的判断必须是你现实的主要来源.没有可以取代技能的指标.

如果您没有这种经验或判断,请考虑与某人签约.

两个粗略的辅助指标是总代码覆盖率和构建速度.

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

是.花在定制系统或解决方案上的绝大部分资金都用于生产之后.对质量,人员和技能的投资永远不会过时.

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

您不仅要考虑对人员和技能的投资,还要考虑最重要的是总体拥有成本和系统的预期使用寿命.

我的个人答案在大多数情况下都是"当然",因为我知道它好得多,但我承认可能有例外.

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

都不是.您的方法应该是在您的代码库中添加测试,同时在功能方面取得进展.

同样,它是对人员,技能和代码库质量的投资,因此需要时间.团队成员需要学习如何打破依赖关系,编写单元测试,学习新的习惯,提高纪律和质量意识,如何更好地设计软件等.重要的是要了解当你开始添加测试时,你的团队成员可能不会拥有这些技能尚未达到成功所需的水平,因此停止进度以花费所有时间来添加大量测试是行不通的.

此外,将单元测试添加到任何相当大的项目规模的现有代码库是一项大型工作,需要承诺和持久性.你无法改变一些基本的东西,期望在途中学到很多东西,并要求你的赞助商不要期望通过停止商业价值流来获得任何投资回报率.那不会飞,坦率地说不应该.

第三,您希望在团队中灌输良好的业务焦点价值.质量永远不会以牺牲客户为代价,没有质量就无法快速发展.此外,客户生活在一个不断变化的世界中,您的工作就是让他更容易适应.客户调整需要质量和业务价值流.

你正在做的是偿还技术债务.而您正在为客户提供不断变化的需求.债务逐渐得到回报,情况有所改善,更容易为客户提供更好的服务并提供更多价值.等等.这种积极的动力是你应该瞄准的,因为它强调了可持续发展的原则,并将为你的开发团队,客户和利益相关者维护和改善道德.

希望有所帮助

  • 对于我在Stackoverflow上阅读过的任何主题,这都是最清晰的答案。做得好!如果您还没有这样做,我敦促您考虑就最后一个问题的答案写一本书。 (3认同)
  • 这些花哨的单元测试建议是一般的寿命建议。说真的 (2认同)

Don*_*ows 24

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

是!

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

没有!

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

添加测试(尤其是自动化测试),使得很多更容易保持项目在未来的工作,这使得它显著不太可能,你会船愚蠢的问题给用户.

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

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

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


fle*_*her 15

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

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


hay*_*uhl 13

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

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

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

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

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

而且,这不是一个全有或全无的命题.如果您有一个由四人组成的团队,并且您可以通过将一两个人放在遗留测试任务上来满足您的最后期限,那么一定要这样做.

编辑:

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

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

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


Joe*_*ite 9

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

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

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

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

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

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

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


Hug*_*ett 8

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

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

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


Dre*_*ler 6

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

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

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

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

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

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

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


小智 5

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

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

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

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

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

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