花大量的时间(如果不是更多的话)编写测试而不是实际的代码是正常的吗?


210

我发现测试比他们正在测试的实际代码更加棘手,更难编写。对于我来说,花更多的时间编写测试而不是测试代码并不稀奇。

那是正常的还是我做错了什么?

问题“ 单元测试或测试驱动的开发值得吗?”,“ 我们比实施系统本身花费更多的时间来实施功能测试,这是否正常?”,他们的答案更多地是关于测试是否值得(例如“我们应该完全跳过编写测试吗?”中的内容)。尽管我确信测试很重要,但是我想知道我在测试上花费的时间是否比实际代码多还是正常的,还是仅我一个人?

从我收到的问题的观点,答案和投票的数量来看,我只能认为它是网站上其他任何问题都没有解决的合法问题。


20
轶事,但我发现我在TDD上花费的时间与编写代码所花的时间差不多。这是因为我花了比代码更多的时间在测试上,然后才开始编写测试。
RubberDuck

10
与编写代码相比,您花费的时间也更多。
托尔比约恩Ravn的安徒生

27
同样,测试实际的代码。您只是不向客户运送该零件。
托尔比约恩Ravn的安徒生

5
理想情况下,与编写代码相比,您将花费更多的时间来运行。(否则,您只需要手动完成任务即可。)
Joshua Taylor

5
@RubberDuck:与此相反的经验。有时,当我根据事实编写代码和设计时,已经很整齐了,因此我不需要过多地重写代码和测试。因此,编写测试所需的时间更少。这不是规则,但是我经常发生。
乔治

Answers:


204

我记得在软件工程课程中,一个人花了大约10%的开发时间来编写新代码,而另外90%的时间是调试,测试和编写文档。

由于单元测试将调试和测试工作捕获到了(可能是自动化的)代码中,因此将更多的精力投入到代码中就变得有意义了。实际花费的时间不应超过调试和测试的时间,而无需编写测试。

最后,测试也应兼作文档!一个人应该以使用代码的方式编写单元测试。即测试(和用法)应该很简单,将复杂的东西放入实现中。

如果您的测试很难编写,那么他们测试的代码可能很难使用!


4
现在可能是研究代码为何如此难以测试的好时机:)尝试“展开”并非严格必要的复杂多功能行,例如大量嵌套的二进制/三元运算符...我真的很讨厌不必要的二进制文件/三元运算符,其中也有二进制/三元运算符作为路径之一...
尼尔森

53
我不同意最后一部分。如果您希望获得很高的单元测试覆盖率,则需要涵盖很少见的用例,有时甚至完全违反代码的预期用途。为那些极端情况编写测试可能只是整个任务中最耗时的部分。
otto

我在其他地方已经说过了,但是单元测试往往会运行更长的时间,因为大多数代码倾向于遵循一种“帕累托原理”模式:您可以用大约20%的代码来覆盖大约80%的逻辑。覆盖100%的逻辑(即覆盖所有边缘情况大约需要五倍的单元测试代码)。当然,根据框架的不同,您可以初始化环境以进行多个测试,从而减少所需的总体代码,但这甚至需要进行额外的计划。接近100%的置信度比简单测试主要路径需要更多的时间。
phyrfox 2015年

2
@phyrfox我认为这太谨慎了,它更像是“其他99%的代码是边缘情况”。这意味着其他99%的测试是针对那些极端情况的。
莫兹

@Nelson我同意很难理解嵌套三元运算符,但是我认为它们不会使测试特别困难(一个好的覆盖率工具会告诉您是否错过了可能的组合之一)。IMO,如果软件耦合太紧密,或者依赖于硬连线数据或未作为参数传递的数据(例如,当条件取决于当前时间而没有作为参数传递)时,则很难进行测试。这与代码的“可读性”没有直接关系,当然,在所有其他条件相同的情况下,可读性更好!
Andres F.

96

它是。

即使只进行单元测试,测试中包含的代码也要比实际测试的代码多。没有什么问题。

考虑一个简单的代码:

public void SayHello(string personName)
{
    if (personName == null) throw new NullArgumentException("personName");

    Console.WriteLine("Hello, {0}!", personName);
}

将会进行哪些测试?这里至少要测试四个简单的情况:

  1. 人名是null。实际抛出异常了吗?至少要编写三行测试代码。

  2. 人名是"Jeff"。我们有"Hello, Jeff!"回应吗?那是四行测试代码。

  3. 人名是一个空字符串。我们期望什么输出?实际输出是多少?附带问题:是否符合功能要求?这意味着用于单元测试的另外四行代码。

  4. 人名对于一个字符串来说足够短,但是太长而不能与"Hello, "感叹号结合使用。会发生什么?¹

这需要大量的测试代码。此外,最基本的代码片段通常需要设置代码,该代码初始化被测代码所需的对象,这通常还导致编写存根和模拟等。

如果比率很大,在这种情况下,您可以检查以下几件事:

  • 测试之间是否存在代码重复?它是测试代码这一事实并不意味着该代码应在相似的测试之间重复(复制粘贴):这样的重复将使这些测试的维护变得困难。

  • 有多余的测试吗?根据经验,如果删除单元测试,则分支覆盖率应降低。如果不是,则可能表明不需要测试,因为其他测试已经覆盖了路径。

  • 您是否仅测试应测试的代码?您不应该测试第三方库的基础框架,而只能测试项目本身的代码。

使用冒烟测试,系统和集成测试,功能和验收测试以及压力和负载测试,您甚至还要添加更多的测试代码,因此您不必担心为每个实际代码的LOC拥有四个或五个LOC测试。

关于TDD的注意事项

如果您担心测试代码所花费的时间,则可能是您做错了,即先进行代码,再进行测试。在这种情况下,TDD可能会通过鼓励您在15-45秒的迭代中进行工作(在代码和测试之间进行切换)来提供帮助。根据TDD的支持者,它通过减少您需要执行的测试数量,更重要的是,减少了为测试编写和编写的业务代码的数量,从而加快了开发过程。


¹令n一串的最大长度。我们可以调用SayHello并通过引用传递长度为n -1 的字符串,该字符串应该可以正常工作。现在,在Console.WriteLine步骤中,格式化应以长度为n + 8 的字符串结尾,这将导致异常。可能由于内存限制,即使是包含n / 2个字符的字符串也会导致异常。一个人应该问的问题是,第四项测试是否是单元测试(看起来像单元测试,但与平均单元测试相比,它在资源方面的影响可能更大),以及它是否测试实际的代码或基础框架。


5
不要忘记一个人也可以使用null。stackoverflow.com/questions/4456438/…–
psatek

1
@JacobRaihle我认为@MainMa生产资料的价值personName在千篇一律string,但价值personName加上连接值溢出string
woz 2015年

@JacobRaihle:我编辑了答案以解释这一点。参见脚注。
阿森尼·穆尔琴科(Arseni Mourzenko)2015年

4
As a rule of thumb, if you remove a unit test, the branch coverage should decrease.如果我写了上面提到的所有四个测试,然后删除了第三个测试,覆盖率会降低吗?
Vivek

3
“足够长”→“太长”(在第4点中)?
圣保罗Ebermann

59

我认为区分两种测试策略很重要:单元测试和集成/验收测试。

尽管在某些情况下必须进行单元测试,但常常无可救药。强加给开发人员的毫无意义的指标(例如“ 100%覆盖率”)加剧了这一情况。http://www.rbcs-us.com/documents/Why-Most-Unit-Testing-is-Waste.pdf为此提供了令人信服的论点。考虑积极的单元测试的以下问题:

  • 大量的无意义的测试没有证明业务价值,而只是为了接近100%的覆盖率而存在。在我工作的地方,我们必须为工厂编写单元测试,该单元测试除了创建类的新实例外别无其他。它没有任何价值。或Eclipse生成的那些冗长的.equals()方法-无需测试它们。
  • 为了简化测试,开发人员会将复杂的算法细分为较小的可测试单元。听起来像是一场胜利,对吧?如果您需要打开12个类以遵循一条通用代码路径,则不需要。在这些情况下,单元测试实际上会降低代码的可读性。与此相关的另一个问题是,如果将代码切得太小,最终会导致一定数量的类(或代码段),除了作为另一段代码的子集之外,似乎没有其他理由。
  • 高度coveraged代码重构是很困难的,因为你还需要保持的依赖于它的工作单元测试的丰富只是如此。行为单元测试会加剧这种情况,在这种情况下,您的测试的一部分还会验证类的协作者之间的互动(通常是模拟的)。

另一方面,集成/验收测试是软件质量的重要组成部分,以我的经验,您应该花费大量时间来使它们正确。

许多商店都喝醉了TDD酷玩乐队,但正如上面的链接所示,许多研究表明,其好处尚无定论。


10
+1为重构点。我曾经研究过经过十多年修补和融合的旧产品。仅尝试确定特定方法的依赖项可能会花费一天的大部分时间,然后试图弄清楚如何模拟它们可能会花费更长的时间。五行更改需要200行以上的测试代码,并花费一周的大部分时间,这并不罕见。
TMN 2015年

3
这个。在MainMa的答案测试4中(在学术环境之外)不应进行测试,因为考虑一下在实践中将如何发生……如果一个人的名字接近字符串的最大大小,则出了点问题。不要测试,在大多数情况下,没有代码路径可以检测到它。适当的响应是让框架抛出底层内存不足异常,因为这是实际的问题。
莫兹

3
我一直在为您加油,直到“无需测试.equals()Eclipse生成的冗长方法”。我为equals()compareTo() github.com/GlenKPeterson/TestUtils编写了一个测试工具。 几乎没有我测试过的所有实现。你如何使用集合如果equals()hashCode()不在一起正确,高效地工作?我为您的其余回答再次欢呼,并对其进行了投票。我什至同意某些自动生成的equals()方法可能不需要测试,但是我遇到了许多错误的实现,使我感到不安。
GlenPeterson

1
@GlenPeterson同意。我的一位同事为此目的写了EqualsVerifier。见github.com/jqno/equalsverifier
Tohnmeister

@Ӎσᶎ不,您仍然必须测试不可接受的输入,这就是人们发现安全漏洞的方式。
gbjbaanb 2015年

11

不能一概而论。

如果我需要从基于物理的渲染中实现公式或算法,那很可能是我花了10个小时进行偏执的单元测试,因为我知道丝毫的错误或不精确都会导致几个月后几乎无法诊断的错误。

如果我只想在逻辑上将几行代码组合在一起并给它起一个名字(仅在文件范围内使用),那么我可能根本不会对其进行测试(如果您坚持要求为每个功能编写测试,无一例外,程序员可能会退缩编写尽可能少的功能)。


这是一个非常有价值的观点。这个问题需要更多的背景才能得到充分回答。
CLF

3

是的,如果您谈论的是TDDing,那是正常的。进行自动化测试后,可以确保所需的代码行为。当您首先编写测试时,您将确定现有代码是否已经具有所需的行为。

这意味着:

  • 如果编写的测试失败,则使用最简单的可行方法更正代码比编写测试短。
  • 如果您编写的测试通过了,那么您就没有多余的代码可以编写,与代码相比,实际上花费了更多的时间编写测试。

(这不考虑代码重构,后者旨在减少编写后续代码的时间。通过测试重构来平衡,旨在减少编写后续测试的时间。)

也可以,如果您要在事后编写测试,那您将花费更多时间:

  • 确定所需的行为。
  • 确定测试所需行为的方法。
  • 满足代码依赖性以能够编写测试。
  • 为失败的测试更正代码。

比您实际花费的代码更多。

是的,这是一种预期的措施。


3

我发现这是最重要的部分。

单元测试并不总是与“看是否正确”有关,而是与学习有关。一旦测试了足够多的东西,它就会“硬编码”到您的大脑中,最终您会减少单元测试的时间,并且发现自己编写整个类和方法时无需进行任何测试就可以完成。

这就是为什么此页面上的其他答案之一提到他们在“课程”中进行了90%的测试的原因,因为每个人都需要学习目标的注意事项。

单元测试不仅可以极大地提高您的技能,而且可以节省大量时间,并且是再次检查自己的代码并在过程中发现逻辑错误的好方法。


2

它可能适用于许多人,但要视情况而定。

如果您是先编写测试(TDD),则可能会在编写测试的时间上有所重叠,这实际上对编写代码很有帮助。考虑:

  • 确定结果和输入(参数)
  • 命名约定
  • 结构-放在哪里。
  • 朴素的旧思维

在编写代码后编写测试时,您可能会发现代码不容易测试,因此编写测试会更困难/花费更长的时间。

大多数程序员编写代码的时间比测试要长得多,所以我希望他们中的大多数不会流利。另外,您还需要花更多的时间来理解和利用您的测试框架。

我认为我们需要改变思维方式,即代码要花多长时间以及涉及到单元测试。永远不要在短期内查看它,也永远不要只比较交付特定功能的总时间,因为您不仅要考虑编写的是更好/更少的错误代码,而且代码更容易更改并且仍然可以使用更好/更少的越野车。

在某个时候,我们所有人都只能编写出色的代码,因此某些工具和技术只能在提高技能方面提供很多帮助。如果我只有激光导引的锯子,就好像我不能盖房子。


2

花大量的时间(如果不是更多的话)编写测试而不是实际的代码是正常的吗?

是的。有一些警告。

首先,从大多数大商店以这种方式工作的意义上说,这是“正常的”,因此即使这种方式完全被误导和愚蠢,大多数大商店以这种方式工作的事实也使其成为“正常”。

由此,我并不是要暗示测试它是错误的。我曾在没有进行测试的环境中以及在具有强迫性测试的环境中工作,但我仍然可以告诉您,即使是强迫性测试也比没有测试要好。

而且我还没有做TDD,(他知道,我将来可能会这样做),但是我通过运行测试而不是实际的应用程序来完成大部分的edit-run-debug周期,所以自然地,我工作很多在我的测试中,以避免尽可能多地运行实际的应用程序。

但是,请注意过度测试存在危险,尤其是维护测试所花费的时间。(我主要是为了回答这个问题而写这个答案。)

在罗伊·奥什罗夫(Roy Osherove)的《单元测试的艺术》(曼宁,2009年)的序言中,作者承认参加了一个项目,但由于设计不当的单元测试带来的巨大开发负担,该项目在很大程度上失败了,而该项目必须在整个测试过程中加以维护,因此在很大程度上失败了。开发工作的持续时间。因此,如果您发现自己花了太多时间只做维护测试而无所事事,那并不一定意味着您走在正确的道路上,因为这是“正常的”。您的开发工作可能已经进入了不健康的模式,在这种模式下,可能有必要对测试方法进行彻底的重新思考以保存项目。


0

花大量的时间(如果不是更多的话)编写测试而不是实际的代码是正常的吗?

  • 用于写入单元测试(测试在隔离模块)如果代码是高度耦合的(遗留,没有足够的关注点分离,缺少依赖注入,而不是TDD开发
  • ,如果只能通过GUI代码访问要测试的逻辑,则可以编写集成/验收测试
  • 没有,只要编写集成/验收检验作为GUI代码和业务逻辑分离器(测试并不需要与GUI交互)
  • 没有编写单元测试,如果有顾虑,依赖注入seperaton,代码是测试驱动developped(TDD)
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.