23

当我最初被介绍给 Mocks 时,我觉得主要目的是模拟来自外部数据源的对象。这样我就不必维护一个自动化的单元测试测试数据库,我可以伪造它。

但现在我开始以不同的方式思考它。我想知道 Mocks 是否更有效地用于将测试方法与其外部的任何东西完全隔离开来。不断浮现在脑海中的图像是您绘画时使用的背景。你想防止油漆弄脏所有东西。我只是在测试那个方法,我只想知道它对这些伪造的外部因素有何反应?

这样做似乎非常乏味,但我看到的优势是当测试失败时,这是因为它被搞砸了,而不是 16 层。但是现在我必须进行 16 次测试才能获得相同的测试覆盖率,因为每个部分都将单独测试。此外,每个测试都变得更加复杂,并且与它正在测试的方法联系更紧密。

这对我来说是正确的,但它也似乎很残酷,所以我有点想知道其他人的想法。

4

9 回答 9

19

我建议你看一下 Martin Fowler 的文章Mocks Aren't Stubs,以获得比我能给你更权威的 Mocks 处理。

模拟的目的是在隔离依赖项的情况下对代码进行单元测试,以便您可以真正在“单元”级别测试一段代码。被测试的代码是真正的交易,它所依赖的每一段代码(通过参数或依赖注入等)都是一个“模拟”(一个空的实现,当它的一个方法被调用时总是返回预期的值。)

模拟一开始可能看起来很乏味,但是一旦你掌握了使用它们的窍门,它们就会使单元测试变得更加容易和健壮。大多数语言都有 Mock 库,这使得模拟相对微不足道。如果您使用 Java,我会推荐我个人最喜欢的:EasyMock。

让我结束这个想法:您也需要集成测试,但是拥有大量的单元测试可以帮助您找出哪个组件包含错误,何时存在。

于 2008-09-12T15:20:04.260 回答
15

不要走黑暗的路,卢克大师。:) 不要嘲笑一切。你可以,但你不应该......这就是为什么。

  • 如果您继续孤立地测试每种方法,那么当您将它们全部组合在一起时,您就会有惊喜并为您完成工作,就像BIG BANG一样。我们构建对象,以便它们可以一起解决更大的问题。它们本身微不足道。您需要知道所有协作者是否按预期工作
  • 模拟通过引入重复使测试变得脆弱- 是的,我知道这听起来令人震惊。对于您设置的每个模拟期望,您的方法签名存在的地方有 n 个。实际代码和您的模拟期望(在多个测试中)。更改实际代码更容易......更新所有模拟期望很乏味。
  • 您的测试现在已获知内部实施信息。因此,您的测试取决于您选择如何实施解决方案......不好。测试应该是一个独立的规范,可以通过多种解决方案来满足。我应该可以自由地在代码块上按删除并重新实现,而不必重写测试套件。因为要求仍然保持不变。

结束时,我会说“如果它像鸭子一样嘎嘎叫,像鸭子一样走路,那么它可能就是一只鸭子” - 如果感觉不对......它可能是。*使用模拟来抽象出问题子项,例如 IO 操作、数据库、第三方组件等。像盐一样,其中一些是必要的。太多了和 :x *
这是基于状态的测试与基于迭代的测试的圣战.. 谷歌搜索会给你更深入的洞察力。

澄清:我在这里遇到了一些阻力wrt集成测试:)所以澄清我的立场..

  • 模拟不在“验收测试”/集成领域中。你只会在单元测试世界中找到它们。这就是我在这里的重点。
  • 验收测试是不同的,并且非常需要 - 不要贬低它们。但是单元测试和验收测试是不同的,应该保持不同。
  • 组件或包中的所有协作者不需要相互隔离。就像过度杀伤的微优化一样。他们的存在是为了共同解决问题......凝聚力。
于 2008-09-12T15:14:28.717 回答
2

是的我同意。我认为模拟有时是痛苦的,但通常是必要的,因为你的测试真正成为单元测试,即只有你可以让你的测试关注的最小单元在测试中。这使您可以消除任何其他可能影响测试结果的因素。你最终会得到更多的小测试,但是找出你的代码问题出在哪里变得容易得多。

于 2008-09-12T15:05:10.520 回答
1

我的理念是你应该编写可测试的代码来适应测试,
而不是编写测试来适应代码。

至于复杂性,我的观点是测试应该写得简单,因为如果是的话,你会写更多的测试。

我可能同意,如果您正在模拟的类没有测试套件,这可能是一个好主意,因为如果他们确实有一个适当的测试套件,您就会知道问题出在哪里而无需隔离。

我使用模拟对象的大部分时间是当我编写测试的代码是如此紧密耦合(阅读:糟糕的设计)时,当它们依赖的类不可用时,我必须编写模拟对象。确实有模拟对象的有效用途,但如果您的代码需要使用它们,我会再看一下设计。

于 2008-09-12T15:17:16.433 回答
1

是的,这是使用模拟测试的缺点。你需要做很多工作,这感觉很残酷。但这就是单元测试的本质。如果你不模拟外部资源,你怎么能孤立地测试一些东西?

另一方面,您正在嘲笑缓慢的功能(例如数据库和 i/o 操作)。如果测试运行得更快,那会让程序员感到高兴。没有什么比等待非常慢的测试更痛苦的了,当您尝试实现一项功能时,这需要超过 10 秒才能完成运行。

如果您项目中的每个开发人员都花时间编写单元测试,那么这 16 层(间接层)不会有那么大的问题。希望您应该从一开始就拥有该测试覆盖率,对吗?:)

另外,不要忘记在协作的对象之间编写功能/集成测试。否则你可能会错过一些东西。这些测试不需要经常运行,但仍然很重要。

于 2008-09-12T15:27:16.877 回答
1

在一个尺度上,是的,模拟旨在用于模拟外部数据源,例如数据库或 Web 服务。但是,在更细粒度的范围内,如果您正在设计松散耦合的代码,那么您可以几乎任意地在代码中画线,以了解在任何时候可能是“外部系统”。拿一个我目前正在做的项目:

当有人尝试签入时,CheckInUiCheckInInfo对象发送到CheckInMediator对象,该对象使用CheckInValidator对其进行验证,然后如果没问题,它会使用CheckInInfoAdapterCheckInInfo填充名为Transaction的域对象,然后将Transaction传递给ITransactionDao的实例.SaveTransaction()用于持久性。

我现在正在编写一些自动化集成测试,显然CheckInUiITransactionDao是外部系统的窗口,它们应该被嘲笑。但是,谁说 CheckInValidator在某些时候不会调用 Web 服务?这就是为什么当你编写单元测试时,你假设除了你的类的特定功能之外的所有东西都是一个外部系统。因此,在我对CheckInMediator的单元测试中,我模拟了它与之对话的所有对象。

编辑: Gishu 在技术上是正确的,并不是所有的东西都需要被模拟,例如我不模拟CheckInInfo,因为它只是一个数据容器。然而,任何你可以看到作为外部服务的东西(它几乎是任何转换数据或具有副作用的东西)都应该被嘲笑。

我喜欢的一个类比是将适当松散耦合的设计视为一个领域,人们站在它周围玩接球游戏。当有人传球时,他可能会向下一个人扔一个完全不同的球,他甚至可能会连续向不同的人扔多个球,或者扔一个球等着接回来再扔给另一个人。这是一个奇怪的游戏。

现在,作为他们的教练和经理,您当然想检查您的团队作为一个整体是如何运作的,因此您可以进行团队练习(集成测试),但您也可以让每个球员自己练习对抗后卫和投球机(单元测试)嘲笑)。这张照片唯一缺少的部分是模拟期望,所以我们的球上涂上了黑色焦油,所以当它们击中后挡板时会弄脏它。每个逆止器都有一个人瞄准的“目标区域”,如果在练习跑结束时目标区域内没有黑色标记,你就知道有问题了,这个人需要调整他的技术。

真的要花时间正确地学习它,我理解 Mocks 的那一天是一个巨大的 a-ha 时刻。将它与控制容器的反转结合起来,我永远不会回去。

顺便说一句,我们的一个 IT 人员刚进来给了我一台免费的笔记本电脑!

于 2008-09-12T15:43:34.310 回答
1

正如有人之前所说,如果你模拟所有东西以隔离比你正在测试的类更细化的东西,你就会放弃在被测试的代码中强制执行内聚。

请记住,模拟具有基本优势,即行为验证。这是存根不提供的东西,也是使测试更加脆弱(但可以提高代码覆盖率)的另一个原因。

于 2008-09-23T15:39:11.380 回答
1

发明模拟部分是为了回答这个问题:如果对象没有 getter 或 setter,你将如何对它们进行单元测试?

这些天来,推荐的做法是模拟角色而不是对象。使用 Mocks 作为一种设计工具来讨论协作和职责分离,而不是作为“智能存根”。

于 2008-10-18T14:40:36.953 回答
0

模拟对象 1) 通常用作隔离被测代码的一种手段,但 2) 正如 keithb 已经指出的那样,对于“关注协作对象之间的关系”很重要。这篇文章给出了一些与主题相关的见解和历史:Responsibility Driven Design with Mock Objects

于 2010-07-18T16:27:15.887 回答