什么时候不进行单元测试?


138

我在一家小公司工作,担任单人开发人员。实际上,我是该公司唯一的开发人员。我有几个(相对)定期编写和维护的大型项目,但是没有一个项目可以支持它们。在开始新项目时,我经常想知道是否应该尝试TDD方法。这听起来是个好主意,但老实说,我永远无法证明所涉及的额外工作。

我努力工作以在设计中具有前瞻性。我意识到,肯定有一天另一位开发人员将不得不维护我的代码,或者至少要对其进行故障排除。我将事情保持尽可能简单,并评论和记录难以掌握的事情。而且事实是,这些项目没有那么大或太复杂,以至于一个体面的开发人员很难理解它们。

我看到的许多测试示例都涉及细节,涵盖了代码的所有方面。由于我是唯一的开发人员,而且我非常接近整个项目中的代码,因此遵循写后手动测试模式的效率要高得多。我还发现需求和功能的变更足够频繁,以至于维护测试会给项目增加相当大的阻力。原本可以用来解决业务需求的时间。

因此,我每次都得出相同的结论。投资回报率太低。

我偶尔会进行一些测试,以确保我正确编写了算法,例如根据员工的聘用日期计算他们在公司的工作年限。但是从代码覆盖率的角度来看,我已经涵盖了大约1%的代码。

在我的情况下,您是否仍会找到一种使单元测试成为常规做法的方法,还是我有理由避免这种开销?

更新: 关于我的情况,我遗漏了一些东西:我的项目都是Web应用程序。为了覆盖我的所有代码,我必须使用自动化的UI测试,在这个领域中,与手动测试相比,我仍然没有太大的收获。


1
感谢大家。我在这里学到很多东西。关于我的情况,我遗漏了一些东西:我的项目都是Web应用程序。为了覆盖我的所有代码,我必须使用自动化的UI测试,在这个领域中,与手动测试相比,我仍然没有太大的收获。
Ken Pespisa

1
使用Telerik的网络自动化测试工具,我们在Transactis上取得了巨大的成功。我们已经将许多以前的手动浏览器测试转换为自动化。自动化测试速度更快,并且对于突出显示您的网站可能存在的任何性能问题也非常有用。
约翰·卡斯特

2
我见过一个试图对整个网页进行自动浏览器测试的项目。据我所知,它没有发现我们通过手动测试发现的数百个严重错误中的任何一个,并且花费了大量的时间进行开发和维护。(使用由NUnit驱动的Selenium)。更糟糕的是,由于浏览器和测试框架的不兼容性,某些测试经常会因非问题而中断。
O'Rooney

1
这并不是一个真正的答案,只是一个观察……您反对单元测试的论点是因为“需求变化太频繁”使我想起了我听到的相反论点:“我们的程序是如此静态,测试的重点是什么它几乎永远都不会改变!” ;)
Bane 2014年

2
Web应用程序的自动化UI测试不是单元测试,它们是完全不同的野兽,如果您不想执行这些操作,我不会怪您。但是您所有的业务代码都应该在后端,这就是您应该测试的内容。
Nyamiou The Galeanthrope'8

Answers:


84

我看到的许多测试示例都涉及细节,涵盖了代码的所有方面。

所以?您不必测试所有内容。只是相关的东西。

由于我是唯一的开发人员,而且我非常接近整个项目中的代码,因此遵循写后手动测试模式的效率要高得多。

这实际上是错误的。效率不高。这真的只是一个习惯。

其他单独开发人员所做的工作是编写草图或轮廓,编写测试用例,然后用最终代码填充轮廓。

那是非常非常有效的。

我还发现需求和功能的变更足够频繁,以至于维护测试会给项目增加相当大的阻力。

这也是错误的。测试不是阻力。需求变化是拖累。

您必须修复测试以反映要求。无论是细节还是高级;首先写或最后写。

在测试通过之前,代码没有完成。那就是软件的普遍真理。

您可以进行有限的“这里是”验收测试。

或者,您可以进行一些单元测试。

或者您可以同时拥有。

但是无论您做什么,总会有一个测试来证明该软件有效。

我建议您一点点的形式化和漂亮的单元测试工具套件会使该测试更加有用。


8
我喜欢您的第一句话,只是测试相关内容。关于手动测试与单元测试的效率,我不相信我的陈述是完全错误的,也不是你的说法完全正确。似乎要在自动测试和手动测试之间找到平衡,以实现最高效率。
Ken Pespisa

9
@肯·佩斯皮萨:对不起。我大约两年前喝了TDD Kool-Aid(经过30年的最后测试)。现在,我坚持测试优先。它使我的工作效率提高了很多,因为在构建时我的想法很少。
S.Lott

3
@Ken Pespisa:不。答案是平衡的。如果您问是否除以零是正确的,那么您会得到压倒性的,出于某种原因的反应。如果询问是否sqrt(-1)应为整数,则会得到压倒性的回复,而这只是一种方式。平衡围绕“如何”和“什么顺序”。绝对的事实是您必须进行测试。因此,请首先编写测试并确保它们可以正常工作。
S.Lott

21
考虑单元测试接口而不是实现细节。测试限制和边界情况。测试有风险的代码。尽管检查代码比检查别人的代码更容易出错,但很多代码很简单,可以通过检查进行验证。第一次进行手动测试可能会更有效率,而第十次进行自动化测试时可能会更有效。
BillThor

10
“直到测试通过,代码才完成”-IMO,不是。代码通过测试后即开始。直到使用了一年或两年,并经过庞大,活跃和急躁的用户群进行的压力测试和集成测试后,代码才可以使用。那是唯一真正重要的测试。
矢量

107

想象一下,您有一整套的测试可以眨眼就能亮起绿色或红色。想象一下,这组测试可以测试所有内容!想象一下,运行测试套件所需要做的就是键入^ T。这会给您带来什么力量?

您可以在不担心破坏某些内容的情况下对代码进行更改吗?您可以添加新功能而不必担心破坏旧功能吗?您能快速清理凌乱的代码而不必担心造成损害吗?

是的,您可以做所有这些事情!随着时间的流逝,您的代码会发生什么?它将变得越来越清洁,因为没有清洁风险。

假设您的肩膀上有一个小仙女。每次您编写一行代码时,妖精都会在测试套件中添加一些内容,以测试该行代码是否达到了预期的目的。因此,每隔两秒钟您可以点击^ T,并看到编写的最后一行代码有效。

您认为应该进行多少调试?

如果这听起来像是幻想,那您是对的。但是现实并没有太大的不同。只需几秒钟即可替换眨眼,而将TDD学科替换为仙女,您几乎已经做到了。

假设您要回到一年前构建的系统,却忘记了如何创建中心对象之一。有一些测试可以以各种方式创建该对象。您可以阅读这些测试并慢跑您的记忆。需要调用API吗?有一些测试会以各种方式调用该API。这些测试是小的文档,用您理解的语言编写。它们是完全明确的。他们是如此正式以至于执行。而且他们不能与应用程序不同步!

不值得投资吗?你在开玩笑吧!谁会不想要那套测试?帮自己一个忙,不要再为愚蠢而吵架了。学习做好TDD,并观察执行速度有多快,代码有多干净。


28
哇,鲍伯叔叔?在这里表达您的想法真是太好了。我同意您对TDD的好处,真的没有论据可言。问题是关于时间的投资和投资回报率。考虑这些事情对我来说并不愚蠢。想象一下,完成一个TDD相比,一个项目要花我50%的时间,而童话告诉我,在项目的整个生命周期中,与手动测试相比,它只节省10%的时间。这似乎是一个幻想,但我认为在某些项目中这完全是合理的。
肯·佩斯皮萨

11
@Ken“想像一下,完成一个TDD会比没有一个项目多花费我50%的时间”。在我看来,这听起来像是幻想。实际上,听起来您只是在现场提出了这个建议,而没有丝毫证据来支持它。
Rein Henrichs

18
@Rein Henrichs-当然我把这个数字加起来,这是一个假设的陈述。我要指出的是,TDD会为项目增加大量时间,因此我必须考虑是否要获得相等或更好的价值。我坚信,您不必说服TDD的价值。但这不是万能药。
肯·佩斯皮萨

11
@Rein,“可用证据”到底是什么?请详细说明。
肯·佩斯皮萨

22
@鲍勃叔叔“用几秒钟的时间来代替眨眼”:当然,你在开玩笑。TDD是一个很好的工具,但是您只需要测试相关的部分,否则您将花费​​更多的时间维护测试,而不是进行任何认真的开发。当需求变化非常快时,尤其如此:您一直在编写和扔掉不断变化的类的测试。我并不是说TDD不好,它必须明智地使用,而不能像您所建议的那样机械地应用。
Giorgio

34

您所犯的错误是,您将测试视为时间投资,没有立即获得回报。它不一定能那样工作。

首先,编写测试确实使您着重于代码的这一部分需要做什么。

其次,运行它们会发现测试中否则会出现的错误。

第三,运行它们有时会显示本来不会在测试中出现的错误,然后真的会在生产中咬你一口。

第四,如果您在正在运行的系统中遇到了一个错误并为其创建了单元测试,则以后将无法重新引入该错误。那可能是一个很大的帮助。重新引入的错误很常见而且很烦人。

第五,如果您需要将代码移交给其他人,则测试套件将使他们的工作变得更加轻松。另外,如果您忽略了一个项目并在几年后重新使用它,那么您将不再那么接近它了,它也将对您有所帮助。

我一直以来的经验是,在项目的整个开发过程中,进行良好的单元测试始终会使过程更快,更可靠。


2
@Ken,测试套件是可执行形式的规范。

32

JUnit(Java单元测试框架)的人有一种哲学,即如果测试太简单,就不要对其进行测试。我强烈建议您阅读他们的“ 最佳做法常见问题解答”,因为它非常实用。

TDD是编写软件的不同过程。单元测试的基本前提是,您将减少调试器中单步执行代码的时间,并更快地确定您的代码更改是否意外破坏了系统中的其他内容。这与TDD相符。TDD周期如下所示:

  1. 编写测试
  2. 观看失败(证明您有事要做)
  3. 只需写下使测试通过所需的内容即可。
  4. 观看通过(是!)
  5. 重构(使其更好)
  6. 洗涤,漂洗并重复

应用TDD不太明显的是它改变了您编写代码的方式。通过强迫自己考虑如何测试/验证代码是否正常,您正在编写可测试的代码。而且由于我们正在谈论单元测试,所以通常意味着您的代码变得更加模块化。对我而言,模块化和可测试的代码是一个巨大的收获。

现在,您需要测试诸如C#属性之类的东西吗?想象一下这样定义的属性:

bool IsWorthTesting {get; set;}

答案是“否”,这不值得测试,因为此时您正在测试语言功能。只需相信C#平台的家伙做对了。此外,如果它失败了,有什么可以做些什么来解决这个问题?

另外,您会发现代码的某些部分非常费力地进行正确测试。这意味着不要这样做,但请确保您测试棘手问题使用/使用的代码:

  • 已检查的异常,仅在安装失败后才会发生。Java有很多这样的东西。您需要编写catch块或声明已检查的异常,即使不对已安装的文件进行破解也无法使其失败。
  • 用户界面。找到被测控件,并调用正确的事件来模拟用户的操作非常麻烦,在某些情况下是不可能的。但是,如果使用“模型/视图/控制器”模式,则可以确保模型和控制器已经过测试,而视图部分则由手动测试进行。
  • 客户端/服务器交互。这不再是单元测试,现在是集成测试。写出通过网络发送和接收消息所需的所有部分,但实际上不要通过网络进行。一个好的方法是减少实际通过电线与原始通信进行通信的代码的责任。在您的单元测试代码中,模拟通信对象以确保服务按预期运行。

信不信由你,TDD将帮助您步入可持续发展的步伐。这不是因为魔术,而是因为您有一个紧密的反馈循环,并且能够迅速发现真正愚蠢的错误。解决这些错误的成本基本上是恒定的(至少足以用于计划目的),因为小错误永远不会变成大错误。将其与代码狂暴/调试清除冲刺的突发性进行比较。


24

您必须在测试成本与错误成本之间取得平衡。

为打开文件的功能编写10行单元测试是失败的,失败的原因是“找不到文件”。

一个对复杂的数据结构执行复杂操作的函数-显然是的。

棘手的是介于两者之间。但是请记住,单元测试的真正价值不是测试特定功能,而是测试它们之间的棘手交互。因此,进行一次单元测试,发现一段代码的更改会破坏1000行以外的其他模块中的某些功能,因此值得在咖啡中使用。


23

测试就是赌博。

创建测试是一个赌注,即在一个单元中发生错误而不是通过该测试捕获错误的成本(现在以及以后的所有代码修订中)要比开发测试的成本高。这些测试开发成本包括诸如增加测试工程的工资单,增加的上市时间,由于不编写其他代码而导致的机会成本损失等。

就像任何赌注一样,有时您会赢,有时会输。

有时,漏洞少得多的后期软件会赢得快速但有漏洞的东西的青睐,这些东西会首先进入市场。有时相反。您必须查看特定领域的统计数据,以及管理层想要赌博的数量。

某些类型的错误可能不太可能被制造出来,或者由于任何早期的健全性测试而无法制造出来,以致于统计上不值得花时间来创建其他特定的测试。但是有时,错误的成本是如此之高(医疗,核能等),以至于公司必须输掉一笔赌注(类似于购买保险)。许多应用程序没有那么高的故障成本,因此不需要更高的不经济保险范围。别人做。


3
好答案。真正能够回答我最初问题的少数几个。自从写这篇文章以来,我一直沉浸在测试的世界中(我喜欢它。)在我真正地知道何时使用(或不使用)它之前,我需要更多地了解它。由于此处所述的许多原因,我希望一直使用它。但这最终取决于我的速度,因为最终这是我的时间的赌注,这是在公司/客户的控制之下,而且通常他们专注于项目三角形的底角:en。 wikipedia.org/wiki/Project_triangle
Ken Pespisa

10

我的建议是仅测试您要正常工作的代码。

不要测试您想要出错的代码,并在以后给您造成问题。


9
让我想起了我的牙医所说的话:您不必用牙线清洁所有牙齿,只需要用牙线清洁即可。
肯·佩斯皮萨

我承认这就是让我想到的。;-)
Nick Hodges

8

我经常想知道我是否应该尝试TDD方法。这听起来是个好主意,但老实说,我永远无法证明所涉及的额外工作。

TDD和单元测试不是一回事。

您可以编写代码,然后在以后添加单元测试。那不是TDD,而是很多额外的工作。

TDD是在红灯循环中进行编码的实践。绿灯。重构迭代。

这意味着针对尚不存在的代码编写测试,观察测试失败,修复代码以使测试正常工作,然后使代码“正确”。这通常可以节省您的工作

TDD的优点之一是它减少了对琐事的思考。一站式错误之类的东西消失了。您不必遍历API文档即可确定返回的列表是从0还是1开始。


您能否详细说明一次失误如何消失?您是说要比通过文档搜索更快地得到关于数组索引是从零开始还是从一开始的答案?对我来说似乎不太可能-我在Google上的反应非常快:)
Ken Pespisa

1
实际上,编写TDD是探索API(包括用于记录功能的传统代码库)的绝佳方法。
Frank Shearar 2011年

如果该API发生更改,这也非常有用...您突然遇到一些失败的测试:-)
bitsoflogic 2011年

@Ken Pespisa,绝对更快-根据您认为是0还是1编写代码,运行它,并在需要时进行修复。在大多数情况下,您会是对的,而您将无需再查找它,如果您输入错了,您将在10秒钟之内知道。
Paul Butcher

非常有趣的好处。我有点喜欢
肯·佩斯皮萨

3

我在一个可以测试几乎所有东西的系统上工作。测试中值得注意的执行是PDF和XLS输出代码。

为什么?我们能够测试收集数据并构建用于创建输出的模型的零件。我们还可以测试确定模型的哪些部分将进入PDF文件的部分。我们无法测试PDF看起来是否正常,因为那完全是主观的。我们无法测试PDF的所有部分是否对典型用户可读,因为这也是主观的。或者,如果条形图和饼图之间的选择对于数据集是正确的。

如果输出将是主观的,则几乎没有单元测试可以做值得做的事情。


实际上,这种测试可能是“集成测试”。是的,集成测试要比单元测试难得多,原因之一是有时“正确”的规则非常复杂,甚至是主观的。
sleske 2014年

2

在很多情况下,“编写然后手动测试”只需要花费一些时间即可编写几次测试。节省时间来自能够随时重新运行这些测试。

想想看:如果你有一些不错的功能覆盖,你的测试(不要与代码覆盖混淆),并假设你有10个功能-点击一个按钮,意味着你有大约10 你要跟重新做你的测试...当你坐下来喝咖啡时。

您还没有具备测试minutae。如果您不想深入了解具体细节,则可以编写覆盖您功能的集成测试... IMO,某些单元测试的语言和平台(而非代码)测试得太细粒度。

TL; DR确实永远都不适合,因为好处实在太好了。


2

我遇到的两个非常好的答案在这里:

  1. 何时进行单元测试与手动测试
  2. 关于单元测试,什么不测试?

避免察觉到的开销的理由:

  • 立即为您的公司节省时间/节省成本
  • 从长远来看,即使在您离开后,也可能在故障排除/维护/扩展方面节省时间/成本。

您是否不想将出色的产品作为工作质量的证明?用自私的话来说,这样做对您来说不是更好吗?


1
最后的好问题。我绝对为自己的工作感到自豪,并且我的应用程序运行良好(如果我可能这么胆大的话)。但是您是对的-在某些测试的支持下它们可能会更好。我认为我的主要目的是尝试在必须从事该项目的时间内尽可能多地进行有用的测试,而又不要过于着迷于代码覆盖率。
肯·佩斯皮萨

1

专业的开发人员编写单元测试是因为从长远来看,它们可以节省时间。您将早晚测试您的代码,如果您不这样做,则用户将测试它,并且如果以后必须修复错误,则将很难修复它们并产生更多的影响。

如果您编写的代码没有测试且没有错误,那就很好。我不相信您可以用零个错误来编写一个非平凡的系统,所以我假设您正在以一种或另一种方式对其进行测试。

在修改或重构较旧的代码时,单元测试对于防止退化也至关重要。他们不会证明您的更改没有破坏旧代码,但可以给您很大的信心(只要他们通过了:))

我不会回过头来为已经交付的代码编写一整套测试,但是下次您需要修改功能时,我建议尝试为该模块或类编写测试,使覆盖率达到70% +在您应用任何更改之前。看看是否对您有帮助。

如果您尝试一下并且可以诚实地说这没有帮助,那么就足够公平了,但是我认为有足够的行业证据表明,他们在尝试使用该方法时至少使您值得这样做。


我喜欢关于防止回归和增加信心的想法。这些正是我要添加测试的原因。
Ken

1

似乎大多数答案都是赞成TDD的,即使问题不是关于TDD而是关于单元测试。

单元测试或不单元测试背后没有完全客观的规则。但是在很多时候似乎很多程序员都不进行单元测试:

  1. 私人方法

根据您的OOP哲学,您可以创建私有方法以将复杂的例程与公共方法分离。公共方法通常打算在许多不同的地方被调用并经常使用,而私有方法只能由一个类或模块中的一个或两个公共方法真正调用,以实现非常特定的功能。通常,为公共方法编写单元测试就足够了,但对于使某些魔术发生的底层私有方法而言,就足够了。如果私有方法出了问题,则您的公共方法单元测试应该足以检测这些问题。

  1. 您已经知道的事情应该可以工作(或者由其他人测试过的事情)

许多新程序员在初次学习测试时就与之相反,并认为他们需要测试正在执行的每一行。如果您使用的是外部库,并且其功能已经过作者的充分测试和记录,则在单元测试中测试特定功能通常毫无意义。例如,某人可能编写测试以确保其ActiveRecord模型通过数据库的“ before_save”回调对属性保持正确的值,即使该行为本身已经在Rails中进行了全面测试。回调可能正在调用的方法,但不是回调行为本身。导入库的任何潜在问题最好通过验收测试而不是单元测试来揭示。

无论您是否正在执行TDD,这两种方法均适用。


0

肯,我以及其他许多开发人员在您的整个职业生涯中多次得出与您相同的结论。

我相信您会发现(和其他许多其他情况一样)的事实是,为应用程序编写测试的最初投资似乎令人生畏,但如果编写得当并且针对代码的正确部分,则它们确实可以节省很多时间。时间。

我最大的问题是可用的测试框架。我从来没有真正感觉到它们就是我想要的东西,所以我只是推出了自己的非常简单的解决方案。这确实使我了解了回归测试的“阴暗面”。我将分享我在这里所做的基本伪代码片段,希望您能找到适合您的解决方案。

public interface ITest {
    public string Name {
        get;
    }
    public string Description {
        get;
    }
    public List<ITest> SubTests {
        get;
    }
    public TestResult Execute();
}

public class TestResult {
    public bool Succesful {
        get;
        set;
    }

    public string ResultMessage {
        get;
        set;
    }

    private Dictionary<ITest, TestResult> subTestResults = new Dictionary<ITest, TestResult>();
    public Dictionary<ITest, TestResult> SubTestResults {
        get {
            return subTestResults;
        }
        set {
            subTestResults = value;
        }
    }
}

之后唯一棘手的部分是弄清楚您认为什么粒度级别是您正在执行的任何项目的最佳“物有所值”。

与企业搜索引擎相比,构建地址簿显然需要更少的测试,但是基本原理并没有真正改变。

祝好运!


我认为随着时间的推移,我会弄清楚哪种粒度级别是最好的。很高兴听到其他人定期创建测试以使他们明智地进行测试,而不是为每个可能的结果自动编写测试。我对测试的第一个介绍是在必须进行所有测试的那种级别上。实际上,TDD的整个概念似乎遵循了这一口头禅。
肯·佩斯皮萨

我认为您可能会受益于使用SubSpec之类的框架(受BDD启发),因此可以在共享上下文设置的同时获得断言(“ SubTest”)隔离。
约翰内斯·鲁道夫,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.