36

我最近加入了一个大量使用单元测试的团队。没有人能向我解释为什么这种形式的测试如此重要,但他们将其视为法律。

我知道自动化测试的想法是防止回归,但我不知道这怎么会是一个问题。模块化的、面向对象的、简洁的代码,注释很好,不会有回归问题。如果您第一次就正确构建它,并为将来不可避免的功能添加设计进行设计,那么您将永远不需要测试。

而且,这难道不是优雅的错误处理和日志记录应该完成的吗?当您可以确保所有外部依赖项首先仔细检查它们的可用性时,为什么要花费数周时间来讨论断言语句和单元测试?

我是否傲慢地得出这样的结论,即单元测试是“糟糕”代码库的拐杖,这些代码库有缺陷且构建不佳?

这是一个严重的问题。我在任何地方都找不到任何好的答案,如果我质疑自动化测试的目的,我所问的每个人似乎都认为我是一个巨魔。

编辑:谢谢你的回答,我想我现在明白了。我看到一些人投票删除,但我要感谢回答的人;它确实有帮助!

4

9 回答 9

41

没有人是完美的——你最终会犯错。单元测试旨在捕捉和查明错误的位置,以便:

  • 增加对您编写的代码正确性的信心
  • 增加对重构正确性的信心
  • 使跟踪测试阶段引入错误的位置变得更加简单

错误处理和日志记录仅在触发错误时才有帮助;单元测试是在测试而不是生产中触发错误的原因。


考虑以下...

您有一个包含 3 个不同部分的软件,每个部分都有 2 个不同的选项。

     A      C      E
    / \    / \    / \
in-<   >--<   >--<   >-out
    \ /    \ /    \ /
     B      D      F

您可以通过手动输入输入并检查输出来测试这一点——首先您输入一些触发 A、C、E 的输入;然后你会放入一些做 A、C、F 的东西,等等,直到你通过 B、D、F 覆盖所有内容。

但请记住,B、D 和 F 每个都有自己的单独参数和需要测试的流程——我们会说每个可能有 10 个变体。因此,这至少是10*10*10 = 1000您需要检查的不同输入,适用于 A、C、E 情况。通过这 6 个组件有 8 种不同的可能流程,因此您需要检查8000种不同的输入组合,以确保您点击了所有不同的可能输入。

另一方面,您可以进行单元测试。如果您明确定义了每个组件的单元边界,那么您可以为 A 编写 10 个单元测试,为 B 编写 10 个单元测试,以此类推,测试这些边界。这为您提供了总共 60 个组件的单元测试,加上少数(比如每个流 5 个,所以 40 个)集成测试,以确保所有组件都正确绑定在一起。总共有100 个测试有效地完成了相同的功能覆盖。

通过使用单元测试,您将获得等量覆盖所需的测试量减少了大约80倍!这是一个相对微不足道的系统。现在考虑更复杂的软件,其中组件的数量几乎肯定大于 6,并且这些组件处理的可能情况的数量几乎肯定大于 10。您从单元测试而不是集成测试中获得的节省会不断增加。

于 2012-08-11T20:19:49.853 回答
23

简短的回答:是的,你很傲慢。;)

假设你真的完美的,你的代码不仅在你编写的时候是正确和完美的,而且它还考虑到了所有未来的需求。

现在......你怎么知道你的代码是完美和正确的?你需要测试它。如果它没有经过测试,你就不能相信它有效。

这不仅仅是关于回归(因为这意味着代码过去可以工作。如果它从来没有工作怎么办?第一次编写时它是错误的)

我知道自动化测试的想法是防止回归,但我不明白这首先会成为一个问题。模块化的、面向对象的、简洁的代码,注释很好,不会有回归问题。

谁告诉你的?那个人应该被鞭笞。面向对象的代码和其他代码一样容易出错。它没有什么神奇之处,它不是灵丹妙药。归根结底,每当您更改一段代码时,都有可能在某处破坏某些东西。根据所讨论的代码,机会可能更大或更小,但无论代码多么好,除非您对其进行测试,否则您无法确定您没有引入回归。

如果您第一次就正确构建它,并为将来不可避免的功能添加设计进行设计,那么您将永远不需要测试。

但是,您如何在第一次正确构建它?正如我上面所说,要做到这一点,您需要进行测试,以向您展示代码是否有效。但更重要的是,您如何“设计”未来将添加的功能?你甚至还不知道它们是什么。

而且,这难道不是优雅的错误处理和日志记录应该完成的吗?当您可以确保所有外部依赖项首先仔细检查它们的可用性时,为什么要花费数周时间来讨论断言语句和单元测试?

一点都不。

您的代码当然应该处理错误情况,它当然应该记录您需要记录的内容。

但是您仍然需要知道它正确地完成了这一切。而且你需要知道它也能正确处理非错误条件!很高兴知道“如果 SQL 服务器不可用,我们会向用户显示一个很好的错误消息并退出”。但如果它可用呢?那么你的应用程序有效吗?

对于任何重要的应用程序,都有很多事情可能出错。有很多功能、很多代码和很多不同的执行路径。

尝试手动测试它永远不会使用所有这些代码路径。它永远不会在每种情况下测试每个功能的各个方面。即使确实如此,那也只会告诉您“代码今天有效”。明天能用吗?你怎么知道?当然,您的直觉可能会告诉您“从那以后我提交的代码没有破坏任何东西”,但是您怎么知道呢?你需要再次测试它。然后再次。然后再次。

你问单元测试是否是糟糕代码库的拐杖。他们是相反的。他们是检查,医生访问,防止代码库变坏。他们不只是告诉你你的代码是否工作,而是告诉你什么时候工作,更重要的是,什么时候停止工作。你不认为你会引入任何回归吗?你有多确定?你能承担错吗?

于 2012-08-11T20:26:15.610 回答
8

当您第一次开始编写代码时,它可能看起来非常简单并且不需要自动化测试。

但是随着时间的推移,您的代码会增长,需求会发生变化,团队也会发生变化。对自动化测试的需求也将增长。如果没有自动化测试,开发人员将害怕重构代码——尤其是如果他们不是编写代码的人。即使使用精心设计的代码添加新功能也会破坏现有功能。但是代码并不总是经过精心设计。在实践中,您可能需要做出妥协。由于各种原因,您的某些代码不会像您希望的那样干净和可维护。当您意识到您需要自动化测试时,可能无法添加它们,因为您的某些类可能难以测试。要添加第一个自动化测试,您可能必须首先重写或重构大量代码。

如果您从一开始就有自动化测试,这将确保您将代码设计为可自动测试的。

于 2012-08-11T20:19:59.993 回答
4

我还有两点要补充:

  1. 我发现单元测试的最大好处是当我必须编写作为代码客户端的测试时发生的微妙转变。如果很难测试,就很难使用。我经常进行界面更改,最终使用户的生活变得更好。
  2. 编写单元测试是一种文档形式。它告诉人们“这里是如何使用(和滥用)我的课程”。未来的客户应该有很多关于如何使用你的代码的内置示例;单元测试给他们。
于 2012-08-11T23:21:46.970 回答
4

我向我的一位教授提出了同样的问题,并且是长期的导师,他发誓,他在这里提到或被提到的许多人中的一个是:

他说测试是记录代码的好方法,如果您有兴趣了解代码在测试中应该如何表现,它会让您了解它试图做什么,而不必弄清楚它的复杂性,同时你可以检查它是否真的这样做,所以它是双赢的...... :)

虽然我有一种感觉,但他只是说,因为他知道我不喜欢评论代码,但更喜欢自我记录的源代码,这仍然是一种有趣的观点;)

作为旁注,您还应该查看覆盖率,当您进行单元测试或设计测试套件时,您希望覆盖率尽可能接近 100%,这意味着测试已经测试了 100% 的代码,包括所有它可以采用不同的路径,这非常具有挑战性,并且可以使您的测试代码比您的源代码大几倍,尽管您也可以自动化您的测试代码,一个简单的例子是测试一个 sql 数据库,您可以创建一个生成的程序所有可能的 sql 语句和测试它们都被正确执行,我认为 sqlite3 有超过 9100 万行测试,http://www.sqlite.org/testing.html非常有趣......哦,这he也是提倡的。

于 2012-08-11T20:26:28.233 回答
4

我知道自动化测试的想法是防止回归,但我不明白这首先会成为一个问题。

除了已经给出的好建议之外,我还要补充一点,回归是不可避免的。即使在行业工作很短的时间——嗯,足够长的时间来经历多个发布周期——你就会意识到旧的错误往往会再次出现,原因有很多。

一个特别的原因:程序员倾向于一遍又一遍地犯同样的简单错误。例如:当您的一位开发人员重构该日期处理代码时,他将继续做出错误假设,例如二月总是有 29 天,或者更糟的是,所有月份的长度都相同。

回归测试是您对此类事情的唯一防御。

于 2012-08-11T20:26:48.613 回答
4

好的,所以我以前的想法有点像你现在的想法,所以我想我会对你的一些陈述发表评论,并对你的陈述给出另一种观点。也许这可以揭示测试驱动开发真正的不同编码方法。

我知道自动化测试的想法是防止回归,但我不明白这首先会成为一个问题。

回归通常不仅仅是在对先前代码的工作理解不足的情况下进行的修改吗?如果是这样,您如何确保所有开发人员都以这样一种方式记录其代码的功能,以便您可以自动检测到后期是否有人进行了破坏性更改?单元测试可能有帮助吗?

模块化的、面向对象的、简洁的代码,注释很好,不会有回归问题。

如果您假设文档写得非常好以至于几乎不可能误解它,并且对所述程序的每个后续修改都是由足够熟练的编码人员以手术般精确的方式完成的,那么回归可能没有问题事实上绝不会改变旧的行为。

如果您将单元测试视为记录行为的一种方式,它难道不是一种同样或更有效的方式来传达旧代码的功能吗?

测试当然不应该排除文档,但是每个工具都有自己的优点和缺点。

如果您第一次就正确构建它,并为将来不可避免的功能添加设计进行设计,那么您将永远不需要测试。

在我看来,从一开始就构建正确和灵活的一切所涉及的成本和努力是巨大的,并且通常会导致两件事:过于复杂的系统和缓慢的开发速度。

问问自己:灵活性的成本是多少?您是否通过为每个问题构建多个解决方案并促进它们之间的运行时切换来最小化风险?或者您正在构建允许在运行时修补行为的插件系统?这两种策略都非常昂贵,无论是在开发时间上还是在增加项目的复杂性上。

如果您只是立即构建您需要的东西,并尝试作为一个团队保持足够的灵活性,以便在需要时快速编写任何修改,那么您将在每次实施中节省多少时间和精力?

如果您对现有功能进行测试,扩展功能会变得更容易吗?

而且,这难道不是优雅的错误处理和日志记录应该
完成的吗?当您可以确保所有外部依赖项首先仔细检查它们的可用性时,为什么要花费数周时间来讨论断言语句和单元测试?

优雅的错误处理和日志记录可能是一种很好的后备方式,但故障恢复永远不能代替正确性。我觉得我无法进一步评论,因为当您说“优雅的错误处理”和“双重检查可用性”时,我不确定您指的是什么,但这对我来说听起来像是恢复。没有错误比能够从错误中恢复要好得多。

我是否傲慢地得出这样的结论,即单元测试是“坏”代码库的拐杖,这些代码库有缺陷且构建不佳?这是一个严重的问题。我在任何地方都找不到任何好的答案,如果我质疑自动化测试的目的,我问的每个人似乎都认为我是一个巨魔。

您当然可以使用单元测试作为避免文档和良好编码原则的借口。这是我在很多地方看到的。但它也是一个很好的工具,可以添加到您的工具箱中,以提高产品的正确性、简单性和灵活性!祝你的新团队好运。我相信你可以教他们一两件事关于好的文档和其他有助于避免错误的原则,他们可能会带来一些原则,这些原则可以帮助你更快、更省力地开发产品,同时避免指数级的复杂性增长。

于 2012-08-11T23:00:42.957 回答
2

模块化的、面向对象的、简洁的代码,注释很好,不会有回归问题。

即使评论很好,评论也只描述了预期的行为。函数可以对线性集合进行排序,但使用冒泡排序而不是快速排序。到那时,评论就变成了一条红鲱鱼(特别是如果你抱怨排序需要永远,但你争辩说,一个小时后,它会正确排序)。很可能是排序算法的原始设计者打算在非常小的列表上使用冒泡排序,然后在较大的列表上切换到快速排序,但是没有测试来验证这一点,如果不深入研究代码就无法检查根据。

此外,代码库的功能和规范可能会在一个小时内迅速发生变化,具体取决于您或您的商店所做的事情。文档是实际开发的主要障碍。最好有一套测试用例来确保没有回归。

如果您第一次就正确构建它,并为将来不可避免的功能添加设计进行设计,那么您将永远不需要测试。

两个字: 敏捷开发。 设计只是“刚刚好”,并且添加比当时指定的更多的功能在很大程度上违反了YAGNI。你最终会花费更多的时间来开发边缘案例而不是开发主要案例。

而且,这难道不是优雅的错误处理和日志记录应该完成的吗?当您可以确保所有外部依赖项首先仔细检查它们的可用性时,为什么要花费数周时间来讨论断言语句和单元测试?

错误处理告诉您出了点问题。单元测试可以告诉你在哪里。您只能通过适当的测试确保一切都与您的代码库一样。

任何人都可以编写混淆代码并告诉你它是完全正确的。任何继承该代码库的人都无法分辨。单元测试有助于缓解这个问题。

我是否傲慢地得出这样的结论,即单元测试是“坏”代码库的拐杖,这些代码库有缺陷且构建不佳?

一点点。代码库不一定要“坏”才能需要单元测试,它们只需要更改即可。(更改代码库和不需要单元测试的代码不能存在于同一维度中。)

您没有考虑的另一种情况是您继承的代码库,您的客户声称缺少功能或无法正常工作。它可能是其中的一两个部分,但它们有一些非常时髦的依赖关系;你不能只拿你最喜欢的编辑器,然后用大锤浏览它。为了保证您不会影响暂时有效的代码,并希望修复/实现新代码,您必须为您希望更改的模块编写回归测试。

错误的修复成本也更高,它传播的越远,它也越多。在开发中发现的 bug 比在 QA 中发现的 bug 比在发布候选中发现的 bug 比在公共版本中发现的 bug 更便宜。

这里还有一些其他挥之不去的问题,例如编程成本(维护成本是它的主要成本,而不是新开发),以及向遗留代码添加新功能,但在大多数情况下,单元非常重要测试完成。如果您在没有某种单元回归的代码库中工作那么您将花费太多时间来修复测试可能已经涵盖的错误。

于 2012-08-11T21:08:33.887 回答
-1

100% 的单元测试覆盖率允许您的代码从两个不同的角度执行。通常,您的代码仅在程序运行时执行。单元测试以另一种更模块化的方式测试流程。想想会计中的复式簿记。(我听说这是现代资本主义的基石)在这里,每个事件都在两个不同的分类账中出现两次。最后余额应该为零。这种记账方式一直在自我检查。这就是我对单元测试的看法。

我还发现执行测试优先设计使我的代码更清晰,更易于管理。通过首先编写测试,我可以准确地思考类应该做什么。

于 2012-08-12T21:39:16.840 回答