【问题标题】:Should you only mock types you own?你应该只模拟你拥有的类型吗?
【发布时间】:2009-12-15 10:00:30
【问题描述】:

我阅读了 Mark Needham 的 TDD: Only mock types you own 条目,想知道这是否是最佳实践?

请注意,他不是反对嘲笑,而是反对直接嘲笑——他确实说写一个包装器和嘲笑是好的。

【问题讨论】:

标签: mocking


【解决方案1】:

我的回答是“不”。您应该模拟在给定单元测试的上下文中有意义的任何内容。您是否“拥有”模拟类型并不重要。

如今,在 Java 或 .NET 环境中,一切(我的意思是一切)都可以很容易地模拟。因此,没有技术理由去麻烦首先编写额外的包装代码。


我最近(2010 年 11 月)一直在考虑的一些其他想法,表明“仅模拟您拥有的类型”可能是多么不合逻辑:

  1. 假设您确实为第三方 API 创建了一个包装器,然后在单元测试中模拟该包装器。但是,后来,您认为包装器可以在另一个应用程序中重复使用,因此您将其移至单独的库中。因此,现在包装器不再由您“拥有”(因为它在多个应用程序中使用,可能由不同的团队维护)。开发人员是否应该为旧包装器创建一个新包装器?!?并继续递归地做,一层又一层地添加本质上无用的代码?
  2. 假设其他人已经为一些重要的 API 创建了一个很好的包装器,并将其作为可重用的库提供。如果所说的包装器正是我的特定用例所需要的,我是否应该首先为包装器创建一个包装器,使用几乎相同的 API,这样我才能“拥有”它?!?

对于一个具体而实际的示例,请考虑 Apache Commons Email API,它只不过是标准 Java Mail API 的包装器。既然我不拥有它,那么每当我为需要发送电子邮件的类编写单元测试时,我是否应该始终为 Commons Email API 创建一个包装器?

【讨论】:

  • 对不起,但我不知道为什么这比stackoverflow.com/a/31938571/2711378 更受欢迎,这个答案很好地解释了这一点。规则是有道理的。您拥有的库并不意味着在同一个包中定义。您知道何时更改您创建的库的实现。即使它是一个开源库,并且如果您知道它何时更改,那么请继续模拟它。但是,当您不知道实现何时发生变化时,这会变得很困难。天啊,有时 api 可能会改变,这就是我们首先使用包装器的原因。
  • @Amanuel, this 评论说相反。
【解决方案2】:

取决于你是说 mock 还是 mock™…

假设您只是使用模拟框架(例如 Mockito)来创建存根,那么创建您不拥有的类型的存根是完全可以和合理的。

但是,如果您使用 mock 框架(例如 Mockito)来创建 mock™ 对象,那么您最好听从 mock™ 传播者的建议。就我个人而言,我与那个运动失去了联系,所以我不能告诉你 Mark Needham 的建议是否被认为是 kosher。

撇开讽刺不谈,Mark 写的关于在 Hibernate 中模拟 EntityManagers 的内容本身听起来很合理。但我怀疑我们能否从该特定案例中概括出诸如“永远不要模拟您不拥有的类型”之类的规则。有时它可能有意义,有时则没有。

【讨论】:

  • 你能解释一下 mock 和 mock™ 之间的区别吗?我知道这已经十多年了(!),但它似乎仍然相关。
  • 我认为他使用了这两个术语,因为在 mockito 中 mock 也是一个存根
  • @ares 但 Mockito wiki 声明“不要模拟你不拥有的类型”github.com/mockito/mockito/wiki
【解决方案3】:

我喜欢explanation the Mockito project gives 这个问题。

不要模拟你不拥有的类型!

这不是一条硬线,但越过这条线可能会有 反响! (很可能会)

  1. 想象一下模拟第三方库的代码。在对第三个库进行特定升级后,逻辑可能会发生一些变化,但测试 套件将执行得很好,因为它是模拟的。所以后来, 认为一切都很好,毕竟建筑墙是绿色的, 软件已部署,并且...繁荣
  2. 这可能表明当前设计与此第三方库的分离不够充分。
  3. 另外一个问题是第三方库可能很复杂,甚至需要大量模拟才能正常工作。这导致过度 指定的测试和复杂的夹具,这本身就损害了 紧凑和可读的目标。或不涵盖代码的测试 足够了,因为模拟外部系统很复杂。

相反,最常见的方法是围绕外部创建包装器 lib/system,尽管应该意识到抽象的风险 泄漏,过多的低级 API、概念或异常发生 超出包装的边界。为了验证集成 使用第三方库,编写集成测试并制作它们 尽可能紧凑和可读。

【讨论】:

  • 什么意思:“抽象泄漏,太多低级API、概念或异常,超出了包装器的边界”?
  • 单元测试都是关于隔离的。只要您在集成测试中承担责任,我认为模拟不是问题。为测试而包装可能会引入其自身的复杂性:除非它们是微不足道的,否则在某些时候您的包装器也需要进行测试。
  • 对于它的价值,我认为这应该是公认的答案。
  • 我不喜欢这个答案。单元测试应该测试您编写的最小工作单元。他们应该将自己与第三方依赖项隔离开来。使用集成测试来解决这些问题。我不希望来自 3rd 方库的 HTTP 调用和 DB 调用减慢我的单元测试速度。
  • 我完全不明白你引用@H6 的第一点。我确实同意通过升级 3rd 方库测试将通过,但他们建议作为替代方案是什么?在 3rd 方库上创建一个包装器,并对其进行模拟。那么这里的逻辑是什么?我的意思是,在他们提出的解决方案中,我们仍然会遇到同样的问题,测试不会发现第 3 方更改,因为我们模拟了调用它们的包装器,所以除了我们只是创建了一个不做的额外函数之外,有什么区别什么都没有?
【解决方案4】:

我本来想说“不”,但快速浏览了这篇博文后,我可以看出他在说什么。

他专门谈到了在 Hibernate 中模拟 EntityManagers。我反对这一点。 EntityManagers 应该隐藏在 DAO(或类似的)中,而 DAO 是应该被模拟的。测试对 EntityManager 的一行调用完全是在浪费您的时间,并且一旦发生任何变化就会中断。

但如果你确实有第三方代码,你想测试你如何与之交互,无论如何。

【讨论】:

    【解决方案5】:

    恕我直言,所有权问题无关紧要。

    相关问题是 耦合 之一,即您的测试代码指定了什么。您当然不想要指定您碰巧使用的某个库的 API 详细信息的测试代码。这就是你得到的,例如使用 Mockito 直接在您的测试类中模拟库。

    针对这个问题的widespread solution proposal 是围绕库创建一个包装器,然后模拟该包装器。但这有以下缺点:

    • 包装器内的代码未经测试。
    • 包装器可能是一个不完善的抽象,因此包装器的 API 可能需要更改。如果您在许多测试中模拟了包装器,则必须调整所有这些测试。

    因此,我建议将测试与生产代码中的接口完全分离。不要将模拟直接放入测试代码中,而是创建一个单独的存根类来实现或模拟生产接口。然后向存根添加第二个接口,它允许测试进行必要的设置或断言。然后,您只需要调整一个类以防生产接口发生变化——您甚至可以模拟/存根复杂或频繁更改的库的接口。


    注意:所有这些都是假设实际上有必要使用模拟或存根。我没有在这里讨论这个问题,因为它不在 OP 的问题范围内。但真的问问自己是否必须使用模拟/存根。根据我的经验,它们被过度使用了......

    【讨论】:

    • 我打算为第一部分投赞成票……但我不能同意最后一段。创建(可能毫无意义的单一实现)接口并模拟它们不是解决方案,也不是做“手动模拟/存根”。相反,完全避免模拟/存根,并为有意义的外部可观察行为编写测试,测试关注“什么”,而不是“如何”。
    • 我同意。我添加了一个免责声明,即是否要模拟的问题很重要,但超出了这里的范围。
    【解决方案6】:

    我同意马克的说法。不幸的是,你不能模拟所有东西,有些东西你不想模拟,只是因为你对它的正常使用是一个黑盒子。

    我的经验法则是模拟可以使测试快速但不会使测试变得不稳定的东西。请记住,并非所有的假货都一样,Mocks are not Stubs

    【讨论】:

    • 实际上,你可以模拟一切。对我来说,真正的区别不是模拟和存根之间,而是严格和非严格期望之间的区别。与任何模拟对象相关的期望可以是严格的,也可以不是。
    【解决方案7】:

    我当然是少数派,但我将 Mocking 视为一种代码气味,并尽可能使用依赖注入。理由是模拟基本上是测试一些难以测试的代码的一种解决方法。模拟削弱了测试,因为它们(充其量)表现得像一个特定版本的库。如果库发生变化,您的测试将失去所有检查值。

    你可以看到上面我使用了 Mark Needham 自己的论点,但并不是说你不应该模拟你不拥有的对象,而是你根本不应该模拟......

    好的,如果依赖注入不是一个选项,那么让我们模拟一下……但是你必须明白你的测试是假的,不会像生产代码那样表现。那不是真正的单元测试,只是部分伪造的。如果可能,您可以通过添加测试来检查模拟对象的行为是否符合预期,从而减少这种情况。

    【讨论】:

    • 我认为您对“依赖注入”或“模拟”的定义可能与通常的不同。它们不是相互排斥的,而是互补的:有时你注入依赖项的真正实现(生产、集成测试),有时你注入模拟(单元测试)。
    • @Ladlestein:我不这么认为,但也许吧。当我更改非显式参数(通常是某个全局库)的行为时,我使用“模拟”。当我重构代码以将隐式参数替换为作为参数提供的显式对象(通过测试对象构造函数或某些方法)时,我谈到了“依赖注入”。事实上,在提供一些测试对象时,我不再谈论 Mock,而是谈论 Stubs(如果它不是真正的实现)或 Fakes(真正的实现,但为了测试目的而简化了)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-02
    • 1970-01-01
    • 1970-01-01
    • 2011-01-05
    • 2019-05-18
    相关资源
    最近更新 更多