【问题标题】:What is Mocking?什么是嘲笑?
【发布时间】:2011-02-09 14:00:49
【问题描述】:

什么是模拟? .

【问题讨论】:

标签: unit-testing mocking


【解决方案1】:

序言:如果你在字典中查找名词mock,你会发现这个词的定义之一是something made as a imitation

Mocking 主要用于单元测试。被测对象可能 依赖于其他(复杂)对象。隔离行为 你想用模拟替换其他对象的对象 模拟真实物体的行为。这很有用,如果真正的 将对象合并到单元测试中是不切实际的。

简而言之,模拟就是创建模拟行为的对象 实物。

有时您可能想要区分 mockingstubbing。关于这个主题可能存在一些分歧,但我对存根的定义是一个“最小”的模拟对象。存根实现了足够的行为以允许被测对象执行测试。

模拟类似于存根,但测试还将验证被测对象是否按预期调用模拟。部分测试是验证模拟是否正确使用。

举个例子:你可以通过实现一个简单的内存结构来存储记录来存根数据库。然后,被测对象可以读取和写入记录到数据库存根以允许它执行测试。这可以测试与数据库无关的对象的某些行为,并且包含数据库存根只是为了让测试运行。

如果您想验证被测对象是否将某些特定数据写入数据库,则必须模拟数据库。然后,您的测试将包含有关写入数据库模拟内容的断言。

【讨论】:

  • 这是一个很好的答案,但它不必要地将模拟的概念限制为 objects。将“object”替换为“unit”会更通用。
  • 我明白 stub 与 mock 的区别。唯一的问题是,如果您使用存根测试您的案例并且它通过了,那么您不能断定您已经在使用存根,因此您不再需要 验证
  • 回来回答我上面的问题。答案是,验证代码是否通过所需函数而不是另一个函数的唯一方法是使用布尔检查。使用该布尔检查是存根和模拟之间的区别。说了很多次,你实际上只是测试了一个函数的输出,所以在这种情况下模拟不适用。
【解决方案2】:

其他答案解释了什么是嘲笑。让我用不同的例子来引导你。相信我,它实际上比你想象的要简单得多。

tl;dr 这是原始类的一个实例。它还注入了其他数据,因此您可以避免测试注入的部分,而只专注测试您的类/函数的实现细节

简单示例:

class Foo {
    func add (num1: Int, num2: Int) -> Int { // Line A 
        return num1 + num2 // Line B
    }
}

let unit = Foo() // unit under test
assertEqual(unit.add(1,5),6)

如您所见,我没有测试 LineA,即我没有验证输入参数。我没有验证 num1, num2 是否为整数。我对此没有任何断言。

我只是测试给定模拟值15 的 LineB(我的实现)是否按照我的预期进行。

显然,实际上这会变得更加复杂。参数可以是自定义对象,例如 Person、Address,或者实现细节可以是多个 +。但是测试的逻辑是一样的。

非编码示例:

假设您正在构建一台机器,用于识别机场安检的电子设备的类型和品牌名称。机器通过处理它用相机看到的东西来做到这一点。

现在你的经理走进门,要求你进行单元测试。

然后,作为开发人员,您可以将 1000 个真实对象(如 MacBook Pro、Google Nexus、香蕉、iPad 等)放在它面前,并测试它们是否都能正常工作。

但您也可以使用模拟对象,例如外观一模一样的 MacBook Pro(没有真正的内部部件)或前面的塑料香蕉。您可以避免投资 1000 台真正的笔记本电脑和腐烂的香蕉。

关键是您不是在尝试测试香蕉是否是假的。也不测试笔记本电脑是否是假的。您所做的只是测试您的机器是否会在看到香蕉时显示not an electronic device,而对于 MacBook Pro,它会显示:Laptop, Apple。对于机器而言,其检测结果对于假冒/模拟的电子产​​品和真实的电子产品应该是相同的。如果您的机器还考虑了笔记本电脑(X 射线扫描)或香蕉的内部结构,那么您的模拟设备的内部结构也需要看起来相同。但您也可以使用不再工作的 MacBook。

如果你的机器测试过设备是否可以开机,那么你需要真正的设备。

上面提到的逻辑也适用于实际代码的单元测试。这是一个函数应该与您从 real 输入(和交互)或您在单元测试期间注入的 mocked 值中获得的真实值相同。就像您如何避免使用真正的香蕉或 MacBook 一样,通过单元测试(和模拟),您可以避免做一些导致服务器返回 500、403、200 等状态代码的事情(强制您的服务器触发 500 仅在服务器关闭时触发,而 200 在服务器启动时触发。

如果您必须在服务器上下切换之间不断等待 10 秒,那么运行 100 次以网络为中心的测试会变得很困难)。因此,您可以使用状态码 500、200、403 等注入/模拟响应,并使用注入/模拟的值测试您的单元/功能。

注意:

有时您没有正确模拟实际对象。或者你不会嘲笑每一种可能性。例如。您的假笔记本电脑是黑色的,您的机器可以准确地与它们配合使用,但是它不能与 白色 假笔记本电脑一起正常工作。后来,当您将这台机器运送给客户时,他们抱怨说它并非一直工作。您会收到随机报告说它不起作用。您需要 3 个月的时间才能弄清楚假冒笔记本电脑的颜色需要更加多样化,以便您可以适当地测试您的模块。

对于一个真实的编码示例,对于状态代码 200(返回图像数据)与 200(未返回图像数据),您的实现可能会有所不同。因此,最好使用提供code coverage 的 IDE,例如下图显示您的单元测试不会通过标有 brown 的行。

image source

真实世界编码示例:

假设您正在编写一个 iOS 应用程序并进行网络调用。您的工作是测试您的应用程序。测试/识别网络调用是否按预期工作不是您的责任。测试它是另一方(服务器团队)的责任。您必须删除此(网络)依赖项,并继续测试所有 围绕它工作的代码。

网络调用可以通过 JSON 响应返回不同的状态代码 404、500、200、303 等。

您的应用应该适用于所有(如果出现错误,您的应用应该抛出预期的错误)。您使用模拟所做的是创建“虚拟的——类似于真实的”网络响应(例如带有 JSON 文件的 200 代码)并测试您的代码“进行真正的网络调用并等待您的网络回复'。您手动硬编码/返回所有类型的网络响应的网络响应,并查看您的应用程序是否按预期工作。 (您永远假设/测试 200 的数据不正确,因为这不是您的责任,您的责任是使用正确的 200 测试 您的应用程序,或者在400、500,你测试你的应用是否抛出了正确的错误)

这种创造想象的——类似于真实的被称为模拟。

为此,您不能使用您的原始代码(您的原始代码没有预先插入的响应,对吧?)。您必须向其中添加一些内容,注入/插入通常不需要(或您的课程的一部分)的虚拟数据。

所以你创建一个原始类的实例并添加你需要的任何东西(这里是网络 HTTPResponse、数据或在失败的情况下,你传递正确的 errorString、HTTPResponse)然后测试 mocked 类。

长话短说,模拟是为了简化限制你正在测试的东西,同时也让你提供一个类所依赖的东西。在此示例中,您避免测试 网络调用自己,而是测试您的应用程序是否按预期运行注入输出/响应——通过模拟

不用说,您分别测试每个网络响应。


现在我一直在想的一个问题是:合约/端点以及基本上我的 API 的 JSON 响应会不断更新。如何编写考虑到这一点的单元测试?

详细说明:假设模型需要一个名为 username 的键/字段。你测试这个并且你的测试通过了。 2 周后,后端将密钥名称更改为 id。你的测试仍然通过。正确的?还是不行?

更新模拟是后端开发人员的责任吗?他们提供更新的模拟是否应该是我们协议的一部分?

上述问题的答案是:单元测试 + 您作为客户端开发人员的开发过程应该/将捕获过时的模拟响应。如果你问我怎么做?答案是:

如果不使用更新的 API,我们的实际应用程序会失败(或者没有失败但没有所需的行为)......因此,如果失败......我们将对我们的开发代码进行更改。这再次导致我们的测试失败......我们必须纠正它。 (实际上,如果我们要正确地执行 TDD 过程,我们不会编写任何有关该字段的代码,除非我们为它编写测试......并且看到它失败然后去为它编写实际的开发代码。)

这一切都意味着后端不必说:“嘿,我们更新了模拟”......它最终通过您的代码开发/调试发生。 ‌ّ因为这都是开发过程的一部分!虽然如果后端为您提供模拟响应,那么它会更容易。

我的全部观点是(如果您不能自动获取更新的模拟 API 响应)可能需要人工交互,即 手动 更新 JSON 并召开简短会议以确保其价值是最新的将成为您流程的一部分

感谢我们的 CocoaHead 聚会小组中的一次闲散讨论

困惑:

我花了一段时间才没有混淆“类的单元测试”和“类的存根/模拟”。 例如。在我们的代码库中,我们有:

  • 类设备
  • 类 DeviceTests
  • 类模拟设备
  • 类 DeviceManager

  • class Device 是实际的类本身。
  • class DeviceTests 是我们为 Device 类编写单元测试的地方
  • class MockDeviceDevicemock 类。我们仅将其用于测试目的。例如如果我们的DeviceManager 需要进行单元测试,那么我们需要Device 类的虚拟/模拟实例。 MockDevice 可用于满足虚拟/模拟实例的需求。

tldr 你使用模拟类/对象来测试其他 对象。您不使用模拟对象来测试自己。


仅适用于 iOS 开发者:

一个很好的模拟例子是Practical Protocol-Oriented talk by Natasha Muraschev。直接跳到 18:30 分钟,虽然幻灯片可能与实际视频不同步?‍♂️

我真的很喜欢成绩单中的这一部分:

因为这是测试...我们确实希望确保 get 函数 从Gettable被调用,因为它可以返回和函数 理论上可以从任何地方分配一系列食物。我们 需要确保它被调用;

【讨论】:

  • 很好的例子,我只想补充一点,在这个特定的例子中,子类充当模拟,但这个例子也使用了存根。硬编码的 JSON 响应被视为存根响应。我之所以添加这个,是因为很难区分 mock 和 stub,但这个示例清楚地展示了两者如何一起使用。
  • 很好的解释,谢谢。对有关 API 更改的问题的一个小调整。如果它不是您的 API,因此您不参与开发过程怎么办?我想知道我的客户端库何时出现故障。
  • @ThinkDigital 好的 API 提供者有很好的发行说明并能正确传达变更,如果你没有那个渠道,那么也许是时候坐下来讨论了 |优秀的开发人员总是会关注新版本的 API 变化,避免只升级 API 版本。你有 API 版本吗?如果他们都没有发现它,那么在 QAing 中你会发现,然后更新你的测试←整个团队的职责。 → 一个开发者的职责:不应该太在意。只处理服务器返回错误的情况,或者服务器不返回错误但无法解析json,或者处理正确的情况。
  • 感谢您的回复,@Honey!就我而言,我正在为pub.dev 维护一个客户端,它有一个 API,但它严重缺乏。以至于通过抓取他们的网站来制作 API 比使用他们的官方 API 更好。因此,对站点的更改可能会破坏代码,在这种情况下,他们无需费心更新任何人。该网站是开源的,但维护一个基于更微不足道的更改的 API 是另一回事。
【解决方案3】:

有很多关于 SO 的答案和网络上关于嘲笑的好帖子。您可能想开始寻找的一个地方是 Martin Fowler Mocks Aren't Stubs 的帖子,他在其中讨论了很多嘲笑的想法。

在一个段落中 - 模拟是一种特殊的技术,允许在不依赖依赖项的情况下测试代码单元。一般来说,模拟与其他方法的不同之处在于,用于替换代码依赖项的模拟对象将允许设置期望 - 模拟对象将知道它应该如何被您的代码调用以及如何响应。


您最初的问题提到了 TypeMock,所以我在下面留下了答案:

TypeMock 是 commercial mocking framework 的名称。

它提供了 RhinoMocks 和 Moq 等免费模拟框架的所有功能,以及一些更强大的选项。

您是否需要 TypeMock 值得商榷 - 您可以使用免费的 mocking 库进行大多数您想要的 mocking,而且许多人认为 TypeMock 提供的功能通常会让您远离封装良好的设计。

正如另一个答案所说,“TypeMocking”实际上不是一个已定义的概念,但可以理解为 TypeMock 提供的模拟类型,使用 CLR 分析器在运行时拦截 .Net 调用,从而提供更大的伪造对象的能力(不是需要接口或虚拟方法等要求)。

【讨论】:

  • @Masoud 从未提到过 TypeMock。他的问题是关于一般的“类型模拟”。
  • @Peter - 正如另一条评论所说,检查问题的编辑历史。如果我发布答案然后原始问题完全改变,我无能为力。
【解决方案4】:

Mock 是一种方法/对象,它以受控方式模拟真实方法/对象的行为。模拟对象用于单元测试。

通常,测试中的方法会调用其中的其他外部服务或方法。这些称为依赖项。一旦被模拟,依赖项的行为方式就是我们定义它们的方式。

通过模拟控制依赖关系,我们可以轻松测试我们编码的方法的行为。这是单元测试。

What is the purpose of mock objects?

Mocks vs stubs

Unit tests vs Functional tests

【讨论】:

    【解决方案5】:

    Mocking 是为测试生成模拟真实对象行为的伪对象

    【讨论】:

      【解决方案6】:

      模拟类型的目的是切断依赖关系,以便将测试隔离到特定单元。存根是简单的代理,而模拟是可以验证使用情况的代理。模拟框架是一种可以帮助您生成存根和模拟的工具。

      编辑:由于最初的措辞提到“类型模拟”,我觉得这与 TypeMock 有关。根据我的经验,一般术语只是“嘲笑”。请随意忽略以下关于 TypeMock 的信息。

      TypeMock Isolator 与大多数其他模拟框架的不同之处在于它可以即时修改 IL。这允许它模拟大多数其他框架无法模拟的类型和实例。要使用其他框架模拟这些类型/实例,您必须提供自己的抽象并模拟它们。

      TypeMock 以干净的运行时环境为代价提供了极大的灵活性。作为 TypeMock 实现其结果的方式的副作用,您有时会在使用 TypeMock 时得到非常奇怪的结果。

      【讨论】:

      • @Masoud 从未提到过 TypeMock。他的问题是关于一般的“类型模拟”。
      • @Peter:最初的措辞是“什么是类型模拟?”。
      • 我知道。由于“类型模拟”不等同于“TypeMock”,我发现你和@Oded 的答案都非常离谱。
      • @Peter:根据我的经验,一般术语是“嘲弄”,但无论如何我已经更新了我的答案,希望能清楚地说明这一点。感谢您的意见。
      【解决方案7】:

      我认为使用 TypeMock 隔离器模拟框架将是 TypeMocking。

      它是一种生成模拟以用于单元测试的工具,无需考虑 IoC 编写代码。

      【讨论】:

      • @Masoud 从未提到过 TypeMock。他的问题是关于一般的“类型模拟”。
      • 其实原题在“Mocking”之前包含了“Type”这个词,但是后来被删掉了。这就是为什么某些答案包含有关 TypeMock 的特定信息的原因。
      【解决方案8】:

      如果您的模拟涉及网络请求,另一种选择是使用真正的测试服务器。您可以使用服务为您的测试生成请求和响应。

      【讨论】:

      • 我刚尝试访问它,花了几分钟。谁说这也不是秘密记录请求?最后,作为评论可能会更好:)
      • 我实际上把它拿下来了,因为我不想把它移到免费主机上。是的,这应该是一个评论。它是开源的,因此如果您担心记录请求,您可以自己运行。 github.com/captainchung/TesterUrl
      猜你喜欢
      • 1970-01-01
      • 2013-02-16
      • 2010-09-07
      • 2019-07-31
      • 1970-01-01
      • 1970-01-01
      • 2014-10-27
      • 2016-10-02
      • 2019-12-20
      相关资源
      最近更新 更多