单元测试反模式目录

203

反模式 :要正式区分实际的反模式与简单的坏习惯,不良实践或错误想法,必须至少有两个关键因素:

  • 某些被重复使用的动作、过程或结构模式,最初似乎有益,但最终会产生更多的负面结果而非获益;
  • 经过重构且有明确文档、在实际应用中被证明并可重复操作的解决方案。

投票选出您“在野外”中见过太多次的TDD反模式。
James Carr的博客文章testdrivendevelopment Yahoo群组上的相关讨论

如果您发现了一个“未命名”的,请也分享出来。每一个反模式请发表一篇帖子以确保投票有意义。

我的目的是找到前N种反模式,以便在不久的将来的午餐会议上讨论它们。


1
这个进展相当不错...谢谢大家。继续加油...在我看来,这是最有信息量的SO帖子之一。 - Gishu
2
+1 喜欢这个帖子!!!而且大部分都是如此真实和普遍存在的! - Chii
2
因为这有点像一项调查 - 你不想仅仅因为发布了最常见的反模式类型而收获声望。;) - Gishu
我看到的大多数答案都是关于单元测试反模式,而不是TDD反模式。例如,“Happy Path”是一个QA反模式,但对于TDD来说完全有效。在我看来,TDD是通过优先选择“Happy Path”并忽略代码覆盖率来实现足够的工作量。那么我们能否更改问题标题,使其更好地适应答案呢? :-) - k3b
@k3b - 我同意。这不是测试驱动开发实践中的反模式。 - Gishu
显示剩余2条评论
31个回答

70

二等公民 - 测试代码没有像生产代码一样进行良好的重构,存在大量重复代码,使得维护测试变得困难。


67

免费搭车/顺风车 -- James Carr, Tim Ottinger
与其编写一个新的测试用例方法来测试另一个/不同的功能特性,一种新的断言(以及其对应的操作,即 AAA 中的 Act 步骤)会在现有的测试用例中顺带而来。


15
是的,那是我最喜欢的。我一直都这样做。哦...等等...你说这是件坏事。 :-) - guidoism
1
我不太确定这是否是反模式。每个可能的mutator调用后,所有不变量都必须为“true”。因此,您将希望在测试每个mutator和输入数据组合后检查每个不变量是否为“true”。但是,您将希望减少重复,并确保检查所有不变量,包括那些目前不会导致测试失败的不变量。因此,您将它们全部放入一个checkInvariants()验证函数中,并在每个测试中使用它。代码更改并添加了另一个不变量。当然,您也会将其放入该函数中。但它是一个免费乘客。 - Raedwald
2
@Raedwald - 随着时间的推移,测试名称不再与其测试的所有内容匹配。此外,由于测试交织在一起,您可能会出现一些抖动;失败并不能指出失败的确切原因。例如,这个测试的一个典型例子可能会写成:所有安排步骤的不透明超集>>行动>>断言A>>再行动>>断言B>>再行动>>断言C。现在,如果A和C都出了问题,你应该会看到2个测试失败。但是使用上述测试,你只会看到一个失败,然后你修复了A,在下一次运行时,它会告诉你现在C出了问题。现在想象一下5-6个不同的测试融合在一起... - Gishu
1
测试名称不再匹配所有被测试的内容,只有当测试名称与最初存在的后置条件相匹配时才会出现问题。如果您使用方法名称、设置状态和输入数据(方法参数)的组合来命名,则没有问题。 - Raedwald
“失败并不指出失败的确切原因”,没有任何“断言”失败能够表明失败的原因。这需要深入了解实现细节:对于回归失败进行调试,对于某些TDD工作,需要了解开发状态。 - Raedwald
显示剩余2条评论

64

Happy Path(快乐路径)

测试只关注正常情况下的预期结果,而不针对边界和异常情况进行测试。

JUnit反模式


原因:时间紧迫或者极度懒惰。 重构解决方案:花些时间写更多的测试,以摆脱虚假阳性。后一种情况需要用鞭子来解决。 :) - Spoike

59

本地英雄(The Local Hero)

一种测试用例,依赖于特定于编写它的开发环境的某些内容才能运行。这导致测试在开发人员的机器上通过,但是当其他人尝试在别处运行时,则无法通过。

隐藏依赖(The Hidden Dependency)

与本地英雄密切相关,一种单元测试需要在测试运行之前已经存在某些数据。如果数据未被填充,测试将失败,并且很难让开发人员知道测试需要什么或为什么会失败,迫使他们深入查找代码以找出数据来自哪里。


遗憾的是,我们在过去看到太多这种情况,古老的.dll文件依赖于模糊而不同的.ini文件,在任何给定的生产系统上都会不断失效,更不用说在没有与负责那些.dll文件的三名开发人员进行广泛协商的情况下,在您的机器上存留这些文件了。唉。


这是WOMPC开发人员缩写的很好的例子。 “在我的电脑上可以运行!”(通常用于让测试人员闭嘴。) - Joris Timmermans

58

链式测试

一些必须按特定顺序运行的测试,即一个测试更改系统的全局状态(全局变量,数据库中的数据),而下一个测试则依赖于其结果。

在数据库测试中经常看到这种情况。测试不会在teardown()中执行回滚操作,而是提交对数据库的更改。另一个常见原因是对全局状态的更改没有包装在try/finally块中,在测试失败时无法清理。


这个真的很糟糕..打破了测试必须独立的概念。但我在多个地方都读到过它...猜测'流行的TDD'相当混乱。 - Gishu

56

嘲弄(Mockery)
有时候,嘲弄能够很好地辅助开发。但是,有些开发者在模拟未被测试的内容时,可能会失去自我。在这种情况下,一个单元测试包含了太多的模拟、存根和/或伪装,以至于被测试的系统根本没有被真正地测试,而是被用来测试从模拟中返回的数据。

来源:詹姆斯·卡尔(James Carr)的文章。


2
我认为这个问题的原因是你的被测试类有太多的依赖。重构的替代方案是提取可以隔离的代码。 - Spoike
@Spoike;如果你的架构是分层的,那就要看类的角色;一些层比其他层更多地依赖于其它类。 - krosenvold
最近我在一篇受人尊敬的博客中看到了一个模拟实体设置,以便从模拟存储库返回。什么鬼?为什么不直接在第一时间实例化一个真正的实体呢?我自己刚刚被一个模拟接口烧伤了,我的实现到处都在抛出NotImplementedExceptions。 - Thomas Eyde

40

悄无声息的捕手 -- Kelly?
如果抛出了异常,即使实际发生的异常与开发人员预期的异常不同,也会通过测试。
参见:秘密捕手

[Test]
[ExpectedException(typeof(Exception))]
public void ItShouldThrowDivideByZeroException()
{
   // some code that throws another exception yet passes the test
}

那个很棘手也很危险(即让你认为你测试了代码,但每次运行都会爆炸)。这就是为什么我尝试具体说明异常类和消息中的某些唯一性。 - Joshua Cheek

34

检查器
一种单元测试,违反封装性以实现100%代码覆盖率,但是它对对象的运行情况了解太多,任何重构的尝试都会破坏现有测试,并要求对单元测试进行任何更改。


'我如何在不将成员变量公开的情况下进行测试...只是为了单元测试?'


2
原因:过度依赖白盒测试。有一些生成这类测试的工具,比如.NET上的Pex。重构解决方案:测试行为而非具体实现,如果确实需要检查边界值,则让自动化工具生成其余部分。 - Spoike
1
在Moq出现之前,我不得不放弃模拟框架,转而手写我的模拟。这太容易将我的测试与实际实现联系起来,使得任何重构几乎不可能。除了使用Moq,我几乎不会犯这种错误。 - Thomas Eyde

34

过度的设置 -- James Carr
一种需要大量设置才能开始测试的测试。有时需要使用几百行代码来为一个测试准备环境,涉及多个对象,这使得由于所有设置所带来的“噪音”,很难真正确定测试的内容。(来源:James Carr的文章)


我理解过多的测试设置通常意味着 a) 代码结构不佳或 b) 模拟不足,对吗? - Topher Hunt
每种情况都可能不同。这可能是由于高耦合导致的。但通常情况下,这是过度规定的情况,即在场景中指定(模拟预期)每个协作者 - 这使得测试与实现耦合并使它们变得脆弱。如果对协作者的调用是测试的附带细节,则不应在测试中使用。这也有助于保持测试简短和易读。 - Gishu

32

分析探针

这是一种必须使用疯狂、非法或其他不健康的方式来执行任务的测试,例如:使用Java的setAccessible(true)读取私有字段或扩展类以访问受保护的字段/方法,或者必须将测试放置在特定包中以访问包全局字段/方法。

如果您看到这种模式,则表示被测试的类使用了过多的数据隐藏。

与检查器的区别在于,被测试的类试图隐藏甚至需要测试的内容。因此,您的目标不是实现100%的测试覆盖率,而是能够进行任何测试。想象一个只有私有字段、没有参数的run()方法和没有getter的类。没有办法在不违反规则的情况下进行测试。


迈克尔·博格沃特评论:这不是真正的测试反模式,而是实用主义来处理被测试代码中的缺陷。当然,更好的方法是修复这些缺陷,但在第三方库的情况下可能无法实现。

亚伦·迪古拉:我有点同意。也许这篇文章确实更适合作为“JUnit HOWTO”维基而不是一个反模式。有什么评论吗?


1
嗯...这行代码“被测试的类试图隐藏甚至是你需要测试的东西”表明了类和测试之间的权力斗争。如果它应该被测试...它应该以某种公开可达的方式存在...通过类的行为/接口...这有点像违反封装的味道。 - Gishu
2
npellow:Maven2 有一个相应的插件,不是吗? - Aaron Digulla
1
这并不是真正的测试反模式,而是实用主义来处理被测试代码中的缺陷。当然,修复这些缺陷会更好,但在第三方库的情况下可能无法实现。 - Michael Borgwardt
@Michael:啊..你说的是旧代码/第三方代码的情况。如果我没有弄错,这篇文章(大部分)都是处理全新的TDD。对于旧代码来说,可能也可以(虽然我仍然会尝试修复设计,如果只需1-2天的工作)。对于第三方代码,你绝对不应该测试它。例如,我不会为.Net框架中的类编写单元测试...简而言之,你不需要为你不掌控的代码编写测试。你可能想在那里编写接口级别的测试,以便知道新版本的dll是否破坏了你的代码。 - Gishu
1
不知道,它肯定有某种副作用。我会测试这个副作用。我不确定你所说的测试第三方 API 是什么意思,我会建议你将其包装在自己的代码中,以便你可以测试它是否被正确使用,然后针对第三方 API 进行集成测试。不会对第三方代码进行单元测试。 - Joshua Cheek
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接