为什么模拟课程这么糟糕?

时间:2009-10-20 14:47:16

标签: unit-testing testing mocking

我最近和一位同事讨论了嘲笑问题。他说,嘲弄课程非常糟糕,不应该在少数情况下完成。

他说只应该嘲笑接口,否则就是架构故障。

我想知道为什么这句话(我完全信任他)是如此正确?我不知道,并且想要被说服。

我是否想念嘲笑(是的,我读过Martin Fowler's article

9 个答案:

答案 0 :(得分:59)

模拟用于协议测试 - 它测试您将如何使用API​​,以及当API做出相应反应时您将如何做出反应。

理想情况下(至少在很多情况下),应该将API指定为接口而不是类 - 接口定义协议,类定义至少部分实现。

实际上,模拟框架在模拟类方面往往有局限性。

根据我的经验,模拟有点过度使用 - 通常你并不真正对确切的交互感兴趣,你真的想要一个存根 ...但是模拟框架可以用来创建存根,以及你陷入了通过嘲弄而不是顽固来创造脆弱测试的陷阱。尽管如此,这是一个艰难的平衡。

答案 1 :(得分:22)

恕我直言,你的同事意味着你应该program to an interface, not an implementation。如果你发现自己经常嘲笑课程,那就表明你在设计建筑时违背了以前的原则。

答案 2 :(得分:14)

模拟类(与模拟接口相比)很糟糕,因为模拟在后台仍然有一个真正的类,它是继承的,并且在测试期间可能会执行实际实现

当您模拟(或存根或其他)某个界面时,不存在您实际想要模拟执行代码的风险。

模拟课程也会迫使你把所有可能被嘲笑的东西变成虚拟,这非常具有侵入性,可能会导致糟糕的课程设计

如果你想要分离类,它们不应该彼此了解,这就是为什么模拟(或存根或其他)其中一个是有意义的。因此,无论如何都建议对接口进行实现,但其他人已经提到了这一点。

答案 3 :(得分:4)

一般来说,你想要模拟一个界面。

虽然可以模拟常规课程,但是它往往会影响您的课程设计,对于可测试性来说太过分了。诸如可访问性,方法是否是虚拟的等问题都将取决于模拟类的能力,而不是真正的OO问题。

有一个叫做TypeMock Isolator的假冒图书馆可以让你克服这些限制(有蛋糕,吃蛋糕),但它相当昂贵。更好地设计可测试性。

答案 4 :(得分:2)

编辑:由于您已澄清您的同事意味着模拟不好但模拟接口不是,因此以下答案已过时。您应该参考this answer

我说的是模拟和存根defined by Martin Fowler,我认为这也是你同事的意思。

模拟很糟糕,因为它可能会导致测试过度规范。尽可能使用stub并避免模拟。

这是mock和stub之间的差异(来自上面的文章):

  

然后我们可以使用状态验证   这样的存根。

class OrderStateTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }
     

当然这是一个非常简单的测试 -   只是发送了一条消息。   我们没有测试它是发送到   合适的人,或有权利的人   内容,但它会做的说明   重点。

     

使用模拟测试看起来很不错   不同。

class OrderInteractionTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }
}
     

为了在存根上使用状态验证,我需要在>存根上做一些额外的方法来帮助验证。因此,存根实现了MailService,但添加了额外的>测试方法。

答案 5 :(得分:2)

我建议尽可能远离模拟框架。同时,我建议尽可能使用mock / fake对象进行测试。这里的诀窍是你应该与真实对象一起创建内置虚假对象。我在我写的关于它的博客文章中详细解释了它:http://www.yegor256.com/2014/09/23/built-in-fake-objects.html

答案 6 :(得分:0)

与大多数关于实践的问题一样,答案是“它取决于”。

过度使用模拟会导致测试无法真正测试任何内容。它还可以导致测试,这些测试是被测试代码的虚拟重新实现,与特定实现紧密相关。

另一方面,明智地使用模拟和存根可以导致单元测试整齐地隔离并单独测试一件事 - 这是一件好事。

这都是关于节制的。

答案 7 :(得分:0)

模拟类是有意义的,因此可以在开发生命周期的早期编写测试。

即使具体实现可用,也有继续使用模拟类的趋势。当系统的某些部分尚未构建时,还有一种趋势是在项目早期针对模拟类(和存根)进行开发。

一旦构建了一个系统,就必须对其进行测试并继续对其进行测试(用于回归)。在这种情况下,从模拟开始是好的,但应该尽快丢弃它们以支持实现。我看到项目很难实现,因为不同的团队继续反对模拟的行为而不是实现(一旦可用)。

通过对模拟进行测试,您假设模拟是系统的特征。通常这涉及猜测被模拟的组件将做什么。如果您有一个您正在嘲笑的系统规范,那么您不必猜测,但由于在构造期间发现的实际考虑因素,“竣工”系统通常与原始规范不匹配。敏捷开发项目假设这种情况总会发生。

然后开发适用于mock的代码。当事实证明模拟并不真正代表真正的竣工系统的行为时(例如模拟中未见的延迟问题,模拟中未见的资源和效率问题,并发问题,性能问题等)有一堆你现在必须维持的毫无价值的模拟测试。

我认为在开发开始时使用模拟是有价值的,但这些模拟不应该有助于项目覆盖。最好是稍后如果删除模拟并创建适当的集成测试来替换它们,否则系统将不会针对模拟未模拟的各种条件(或相对于真实系统模拟不正确)进行测试。 p>

所以,问题是是否使用模拟,这是何时使用它们以及何时删除它们。

答案 8 :(得分:0)

这取决于您使用模拟的频率(或由于设计不良而被迫使用)。

如果实例化对象变得太难了(并且发生的次数比平时更多),则表明该代码可能需要进行认真的重构或更改设计(生成器?工厂?)。

当您对所有内容进行模拟时,最终会得到测试,这些测试了解有关实现的所有信息(白盒测试)。您的测试不再记录如何使用该系统-它们基本上是其实施的镜像。

然后是潜在的代码重构。 根据我的经验,这是与过度模拟有关的最大问题之一。它变得痛苦并且需要时间,很多。 一些开发人员担心知道需要多长时间才能重构代码。 还有一个目的问题-如果一切都被嘲笑了,我们真的在测试生产代码吗?

当然,模仿通常会通过在两个地方重复代码来违反DRY原理:一次在生产代码中,一次在测试中。 因此,正如我之前提到的,对代码的任何更改都必须在两个地方进行(如果测试编写得不好,则可以在更多地方进行..)。