我啥时候应该嘲笑?

Posted

技术标签:

【中文标题】我啥时候应该嘲笑?【英文标题】:When should I mock?我什么时候应该嘲笑? 【发布时间】:2010-09-07 11:32:11 【问题描述】:

我对 mock 和 fake 对象有基本的了解,但我不确定我对何时/何地使用 mock 有感觉 - 特别是因为它适用于这种场景 here

【问题讨论】:

我建议只模拟进程外依赖项,并且只模拟它们中的那些,与外部可观察到的交互(SMTP 服务器、消息总线等)。不要模拟数据库,这是一个实现细节。更多信息在这里:enterprisecraftsmanship.com/posts/when-to-mock 【参考方案1】:

单元测试应通过单一方法测试单一代码路径。当一个方法的执行传递到该方法之外,进入另一个对象,然后再返回时,您就有了依赖关系。

当您使用实际依赖项测试该代码路径时,您不是在进行单元测试;你是集成测试。虽然这很好且必要,但它不是单元测试。

如果您的依赖有问题,您的测试可能会受到影响,从而返回误报。例如,您可能会向依赖项传递一个意外的 null,并且依赖项可能不会像记录的那样抛出 null。您的测试没有遇到应有的空参数异常,并且测试通过了。

此外,您可能会发现很难(如果不是不可能的话)可靠地让依赖对象在测试期间准确地返回您想要的内容。这还包括在测试中抛出预期的异常。

一个模拟取代了这种依赖。您设置对依赖对象调用的期望,设置它应该为您执行所需测试的确切返回值,和/或抛出哪些异常,以便您可以测试异常处理代码。通过这种方式,您可以轻松地测试相关单元。

TL;DR:模拟单元测试涉及的每个依赖项。

【讨论】:

这个答案太激进了。单元测试可以而且应该使用多个方法,只要它都属于同一个内聚单元。否则将需要太多的模拟/伪造,导致复杂和脆弱的测试。只有不真正属于被测单元的依赖项才应该通过模拟替换。 这个答案也太乐观了。如果能结合@Jan 的mock 对象的缺点就更好了。 这不是专门为测试注入依赖项而不是专门模拟的论据吗?您几乎可以在答案中用“存根”替换“模拟”。我同意您应该模拟或存根重要的依赖项。我见过很多模拟繁重的代码,它们基本上最终重新实现了模拟对象的部分;模拟当然不是灵丹妙药。 模拟单元测试涉及的每个依赖项。这解释了一切。 TL;DR:模拟单元测试涉及的每个依赖项。 - 这不是一个很好的方法,mockito 自己说 - 不要嘲笑一切。 (否决)【参考方案2】:

经验法则:

如果您正在测试的函数需要一个复杂的对象作为参数,并且简单地实例化这个对象会很痛苦(例如,如果它试图建立 TCP 连接),请使用模拟。

【讨论】:

【参考方案3】:

当您尝试测试的代码单元中存在依赖项时,您应该模拟一个对象,该依赖项需要“恰如其分”。

例如,当您尝试测试代码单元中的某些逻辑但您需要从另一个对象获取某些内容时,从该依赖项返回的内容可能会影响您尝试测试的内容 - 模拟该对象。

可以找到有关该主题的精彩播客 here

【讨论】:

该链接现在路由到当前剧集,而不是预期的剧集。预定的播客是 hanselminutes.com/32/mock-objects 吗?【参考方案4】:

当您想要测试被测类和特定接口之间的交互时,模拟对象非常有用。

例如,我们要测试方法sendInvitations(MailServer mailServer) 只调用一次MailServer.createMessage(),并且只调用一次MailServer.sendMessage(m),并且在MailServer 接口上没有调用其他方法。这是我们可以使用模拟对象的时候。

使用模拟对象,我们可以传递MailServer 接口的模拟实现,而不是传递真正的MailServerImpl 或测试TestMailServer。在我们传递一个模拟 MailServer 之前,我们“训练”它,以便它知道期望什么方法调用以及返回什么返回值。最后,模拟对象断言,所有预期的方法都按预期调用。

这在理论上听起来不错,但也有一些缺点。

模拟缺点

如果你有一个模拟框架,你很想使用模拟对象每次你需要将一个接口传递给被测试的类。这样,您最终即使在不必要的情况下也可以测试交互。不幸的是,不需要的(意外的)交互测试是不好的,因为那时您正在测试特定需求以特定方式实现,而不是实现产生所需的结果。

这是一个伪代码示例。假设我们已经创建了一个 MySorter 类并且我们想要测试它:

// the correct way of testing
testSort() 
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]



// incorrect, testing implementation
testSort() 
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....

(在此示例中,我们假设我们要测试的不是特定的排序算法,例如快速排序;在这种情况下,后一种测试实际上是有效的。)

在这样一个极端的例子中,很明显为什么后一个例子是错误的。当我们更改MySorter 的实现时,第一个测试在确保我们仍然正确排序方面做得很好,这就是测试的重点——它们允许我们安全地更改代码。另一方面,后一种测试总是会中断,并且是有害的;它阻碍了重构。

模拟为存根

Mock 框架通常也允许不那么严格的用法,我们不必准确指定方法应该调用多少次以及需要哪些参数;它们允许创建用作stubs 的模拟对象。

假设我们有一个要测试的方法sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)PdfFormatter 对象可用于创建邀请。这是测试:

testInvitations() 
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met

在这个例子中,我们并不真正关心PdfFormatter 对象,所以我们只是训练它安静地接受任何调用,并为sendInvitation() 此时恰好调用的所有方法返回一些合理的预设返回值。我们是如何得出这个训练方法列表的呢?我们只是简单地运行测试并不断添加方法,直到测试通过。请注意,我们训练存根响应一个方法,却不知道它为什么需要调用它,我们只是添加了测试抱怨的所有内容。我们很高兴,测试通过了。

但是当我们更改 sendInvitations()sendInvitations() 使用的其他类来创建更精美的 pdf 时会发生什么?我们的测试突然失败了,因为现在调用了更多的PdfFormatter 方法,而我们没有训练我们的存根来期待它们。通常,在这种情况下失败的不仅仅是一个测试,而是任何碰巧直接或间接使用sendInvitations() 方法的测试。我们必须通过增加更多的培训来修复所有这些测试。还要注意,我们不能删除不再需要的方法,因为我们不知道哪些是不需要的。同样,它阻碍了重构。

另外,测试的可读性严重受损,那里有很多代码不是我们想要写的,而是因为我们不得不写;不是我们想要那里的代码。使用模拟对象的测试看起来非常复杂并且通常难以阅读。测试应该帮助读者理解,测试下的类应该如何使用,因此它们应该简单明了。如果它们不可读,则没有人会维护它们;事实上,删除它们比维护它们更容易。

如何解决这个问题?轻松:

尽可能尝试使用真实类而不是模拟类。使用真实的PdfFormatterImpl。如果不可能,请更改真实的类以使其成为可能。无法在测试中使用某个类通常表明该类存在一些问题。解决问题是一个双赢的局面——你修复了课程并且你有一个更简单的测试。另一方面,不修复它并使用模拟是一个不成功的情况 - 你没有修复真实的类,并且你有更复杂、可读性更低的测试,阻碍了进一步的重构。 尝试创建一个简单的接口测试实现,而不是在每个测试中模拟它,并在所有测试中使用这个测试类。创建什么都不做的TestPdfFormatter。这样一来,您可以为所有测试更改一次,并且您的测试不会因为训练存根的冗长设置而杂乱无章。

总而言之,模拟对象有其用处,但如果不小心使用,它们往往会鼓励不良做法、测试实现细节、阻碍重构并产生难以阅读和难以维护的测试

有关 mock 缺点的更多详细信息,另请参阅 Mock Objects: Shortcomings and Use Cases。

【讨论】:

一个深思熟虑的答案,我基本同意。我想说,由于单元测试是白盒测试,因此当您更改实现以发送更精美的 PDF 时必须更改测试可能不是不合理的负担。有时,模拟可以是快速实现存根而不是使用大量样板的有用方法。然而,在实践中,它们的使用似乎并不局限于这些简单的情况。 不是模拟的全部意义在于您的测试是一致的,您不必担心每次运行您的其他程序员可能会不断更改其实现的对象的模拟测试并获得一致的测试结果? 非常好的和相关的点(尤其是关于测试脆弱性)。我小时候经常使用模拟,但现在我认为严重依赖模拟的单元测试可能是一次性的,并且更多地关注集成测试(使用实际组件) “无法在测试中使用类通常表明该类存在一些问题。”如果该类是服务(例如访问数据库或代理 Web 服务),则应将其视为外部依赖项并模拟/存根 @MichaelFreidgeim 为什么要模拟数据库?为什么不使用嵌入式数据库来测试相同的东西?这样你就没有单独的单元和集成测试。此外,由于数据库是嵌入式的,因此您的测试执行得更快。

以上是关于我啥时候应该嘲笑?的主要内容,如果未能解决你的问题,请参考以下文章

我啥时候应该增加 xcdatamodeld 版本?

我啥时候应该使用 QThread::HighestPriority

我啥时候应该销毁令牌[关闭]

Firebase:我啥时候应该使用 refreshToken?

我啥时候应该在 C 中使用 malloc,啥时候不应该?

我啥时候应该在 C 中使用 malloc,啥时候不应该?