如果您的单元测试代码“闻起来”真的有关系吗?


52

通常,我只是使用复制和粘贴以及所有其他不良做法将单元测试放在一起。单元测试通常看起来很难看,充满了“代码味道”,但这真的重要吗?只要“真实”代码是“良好”,我总是告诉自己,这很重要。另外,单元测试通常需要诸如存根函数之类的各种“臭味”。

我应该如何应对设计不佳(“臭”)的单元测试?


8
修正您一直在复制的代码有多困难?
JeffO 2011年

7
测试中的代码异味很容易表明其余代码中存在隐藏的气味-为什么将测试视为不是“真实”代码?
HorusKol 2011年

Answers:


75

单元测试的气味重要吗?当然是。但是,它们与代码气味不同,因为单元测试具有不同的用途,并且具有不同的张力来通知其设计。代码中的许多异味不适用于测试。考虑到我的TDD心态,我实际上会认为单元测试的气味比代码的气味重要,因为代码正好可以满足测试的需要。

以下是一些常见的单元测试气味:

  • 易碎性:即使看似微不足道或无关的代码更改,您的测试也会经常且意外失败吗?
  • State Leak:您的测试是否会因运行顺序不同而有所不同?
  • 设置/拆解膨胀:您的设置/拆解块很长并且增长了吗?他们执行任何类型的业务逻辑吗?
  • 运行时间慢:您的测试是否需要很长时间才能运行?您的任何单个单元测试是否需要超过十分之一秒的时间才能运行?(是的,我很认真,十分之一秒。)
  • 摩擦:现有测试是否会使编写新测试变得困难?您是否发现重构时经常遇到测试失败的困扰?

气味的重要性在于它们是设计或其他更基本问题的有用指示,即“有烟就有火”。不仅要寻找测试气味,还要寻找其潜在原因。

另一方面,这里是一些单元测试的好习惯:

  • 快速,集中的反馈:您的测试应快速隔离故障,并为您提供有关故障原因的有用信息。
  • 最小化测试代码距离:测试和实现它的代码之间应该有一条清晰而短的路径。长距离会产生不必要的长反馈回路。
  • 一次测试一件事:单元测试只能测试一件事。如果您需要测试其他内容,请编写另一个测试。
  • 错误是您忘记编写的测试:从这次失败中您可以学到什么,以便将来编写更好,更完整的测试?

2
“单元测试具有不同的目的,并具有不同的张力来指导设计。” 例如,它们应为DAMP,而不必为DRY。
约尔格W¯¯米塔格

3
@Jörg同意,但是DAMP实际上代表什么吗?:D
Rein Henrichs

5
@Rein:描述性和有意义的短语。另外,不完全干燥。参见codeshelter.wordpress.com/2011/04/07/…–
罗伯特·哈维

+1。我也不知道DAMP的含义。
egarcia

2
@ironcode如果您不能孤立地在一个系统上工作,而不必担心会破坏它与其他系统的集成,那听起来就像是与我紧密耦合。这正是识别测试气味(如长时间运行)的目的:它们会向您通知设计问题(如紧密耦合)。响应不应为“哦,那测试气味无效”,应为“这种气味告诉我我的设计是什么?” 指定系统外部接口的单元测试应足以告诉您所做的更改是否破坏了与使用者之间的集成。
Rein Henrichs

67

关注。您编写单元测试来证明您的代码以您期望的方式运行。它们使您可以放心地快速重构。如果您的测试易碎,难以理解或难以维护,则您将忽略失败的测试或随着代码库的发展而将其关闭,从而抵消了编写测试的许多好处。


17
+1当您开始忽略失败的测试时,它们不会增加任何价值。

19

我几天前刚读完《单元测试的艺术》。作者提倡在单元测试中和在生产代码中一样注意。

我亲身经历了写得不好,无法维护的测试。我自己写了一些。几乎可以保证,如果测试使维护困难,那么它将不会得到维护。一旦测试与被测代码不同步,它们就成了谎言和欺骗的巢穴。单元测试的全部目的是让人们相信我们没有破坏任何东西(也就是说,它们可以建立信任)。如果测试不能被信任,那将比没用更糟糕。


4
+1必须像生产代码一样维护测试
Hamish Smith

关于此主题的另一本非常好的书是Gerard Meszaros的“ xUnit测试模式-重新测试代码”。
adrianboimvaser

7

只要您的单元测试实际上在“大多数”情况下测试您的代码。(大多数时候是故意说的,因为有时很难找到所有可能的结果)。我认为“臭味”代码是您的个人喜好。我总是以一种可以在几秒钟内阅读并理解它的方式编写代码,而不是深入研究垃圾并试图理解什么。尤其是经过大量时间后重新使用它时。

底线-它的测试应该很容易。您不想将自己与“臭味”代码混淆。


5
+1:不要大惊小怪的单元测试代码。一些最愚蠢的问题围绕使单元测试代码更“健壮”,以便编程更改不会破坏单元测试。傻 单元测试被设计为易碎的。如果它们不如设计用于测试的应用程序代码那么健壮,那也可以。
S.Lott

1
@ S.Lott都同意和不同意,原因在我的回答中得到了更好的解释。
Rein Henrichs

1
@Rein Henrichs:这些气味比问题中描述的严重得多。“复制和粘贴”和“看起来很丑”听起来不像描述测试不可靠的气味。
S.Lott

1
确切地说,@ S.Lott,我想如果我们要谈论“单元测试的气味”,则必须进行区分。单元测试中的代码气味通常不是。它们是为不同的目的而写的,而在一种情况下使它们发臭的张力在另一种情况下则非常不同。
Rein Henrichs

2
@ S.Lott顺便说一句,这似乎是使用被忽略的聊天功能的绝好机会:)
Rein Henrichs

6

绝对是 有人说“任何测试总比没有测试好”。我非常不同意-写得不好的测试会拖延您的开发时间,最终您会浪费大量的时间来修复“损坏的”测试,因为它们一开始就不是好的单元测试。目前对我来说,我要重点关注的两件事是使我的测试有价值而不是负担:

可维护性

您应该测试结果(发生什么),而不是方法(如何发生)。测试的设置应与实现尽可能地分离:仅设置绝对必要的服务调用等结果。

  • 使用模拟框架来确保您的测试不依赖任何外部
  • 在可能的情况下优先使用模拟存根(如果您的框架区分它们)
  • 测试中没有逻辑!如果,开关,for-each,案例,try-catching等都是大忌,因为它们会将错误引入测试代码本身

可读性

可以在测试中允许更多重复,如果这样可以使它们更具可读性,通常您不会在生产代码中允许这样做。只需将其与上述可维护性平衡即可。要明确测试的内容!

  • 尝试维护测试的“安排,执行,声明”样式。这样可以将您的设置和对方案的期望与正在执行的操作和确定的结果区分开。
  • 每个测试维护一个逻辑断言(如果测试名称中包含“和”,则可能需要将其分解为多个测试)

总之,您应该非常关注“臭味”测试-它们最终只会浪费您的时间,没有价值。

您已经说过:

单元测试通常需要诸如存根函数之类的各种“臭名昭著”。

听起来您肯定可以阅读一些单元测试技术,例如使用Mocking框架,从而使您的生活变得轻松得多。我会非常强烈地推荐“单元测试的艺术”,它涵盖了以上内容以及更多内容。在长时间编写不佳,无法维护的“臭”测试之后,我发现它很有启发性。这是我今年做出的最佳投资时间之一!


极好的答案。我只有一个小问题:比“每个测试一个逻辑断言”更重要的是每个测试一个动作。关键是无论安排一个测试要采取多少步骤,都应该只有一个“正在测试的生产代码动作”。如果上述操作应具有多个副作用,则您可能会有多个断言。
幻灭了

5

您有两个问题:

  • 您是否绝对肯定自己正在测试自己认为要测试的内容?
  • 如果其他人查看单元测试,他们是否能够弄清楚代码应该做什么

在单元测试中,有几种处理重复性任务的方法,这是最常见的设置和拆卸代码。本质上,您具有测试设置方法和测试拆卸方法-所有单元测试框架都对此提供支持。

单元测试应该很小并且易于理解。如果不是这样,并且测试失败,那么您将如何在相当短的时间内解决问题。当您不得不返回代码时,请在接下来的几个月中让自己轻松一些。



3

除了这里的其他答案。

单元测试中质量差的代码不仅限于您的单元测试套件。

文档是单元测试所扮演的角色之一。

单元测试套件是寻找API打算如何使用的场所之一。

API的调用者不太可能会复制单元测试套件的一部分,从而导致不良的测试套件代码感染其他地方的实时代码。


3

我几乎没有提交我的答案,因为花了我一段时间才弄清楚如何甚至将此作为一个合法问题来解决。

程序员从事“最佳实践”的原因不是出于美学原因,也不是因为他们想要“完美”的代码。这是因为它节省了他们的时间。

  • 节省时间,因为好的代码更易于阅读和理解。
  • 这样可以节省时间,因为当您需要查找错误时,可以更轻松,更快地找到错误。
  • 这样可以节省时间,因为当您要扩展代码时,这样做更容易,更快捷。

因此,您要问的问题(从我的角度来看)是我应该节省时间还是编写会浪费时间的代码?

对于这个问题,我只能说,您的时间有多重要?

仅供参考:存根,嘲笑和猴子补丁都具有合法用途。他们仅在不适当使用时“闻”。


2

我专门尝试不要让我的单元测试过于鲁棒。我已经看到单元测试以健壮的名义开始进行错误处理。您最终得到的是测试,这些测试吞噬了他们试图捕获的错误。单元测试还必须经常做一些时髦的事情才能使它们工作。考虑一下使用反射的私有访问器的整个想法...如果我在生产代码中看到一堆这样的代码,我将十分担心10次中的9次。我认为应该花更多的时间考虑正在测试的内容,而不是代码清洁度。如果您进行任何重大的重构,那么无论如何都将需要经常更改测试,那么为什么不将它们混在一起,拥有更少的主人翁感,并在时机成熟时更积极地重写或修改它们呢?


2

如果您打算进行大量的低级覆盖,那么进行任何重大修改时,您将在测试代码中花费与在产品代码中一样多的时间或更多的时间。

根据测试设置的复杂性,代码可能会更复杂。(例如,httpcontext.current与试图准确地构建一个假人的可怕怪物)

除非您的产品很少对现有接口进行重大更改,并且您的单位级输入非常容易设置,否则我至少会担心将测试理解为实际产品。


0

代码气味只是代码中的一个问题,需要在以后的某个时刻进行更改或理解。

因此,我认为当您必须“接近”给定的单元测试而不是之前时,应该解决代码错误的问题。

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.