什么是嘲笑? .
序言:如果您在字典中查找名词mock,您会发现该词的定义之一是模仿。
模拟主要用于单元测试。被测对象可能依赖于其他(复杂)对象。要隔离对象的行为,您希望通过模拟真实对象行为的模拟来替换其他对象。如果将真实对象合并到单元测试中是不切实际的,这将很有用。简而言之,模拟就是创建模拟真实对象行为的对象。
有时您可能想要区分模拟和存根。关于这个主题可能存在一些分歧,但我对存根的定义是一个“最小”的模拟对象。存根实现了足够的行为以允许被测对象执行测试。
模拟类似于存根,但测试还将验证被测对象是否按预期调用模拟。测试的一部分是验证模拟是否正确使用。
举个例子:您可以通过实现一个简单的内存结构来存储记录来存根数据库。然后,被测对象可以读取和写入记录到数据库存根以允许它执行测试。这可以测试与数据库无关的对象的某些行为,并且包含数据库存根只是为了让测试运行。
如果您想验证被测对象是否将某些特定数据写入数据库,则必须模拟数据库。然后,您的测试将包含有关写入数据库模拟的内容的断言。
其他答案解释了什么是嘲笑。让我用不同的例子来引导你。相信我,它实际上比你想象的要简单得多。
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 是否为整数。我对此没有任何断言。
我只是在测试给定模拟值 1
和 5
的 LineB(我的实现)是否按我的预期进行。
显然,在现实世界中,这可能会变得更加复杂。参数可以是自定义对象,例如 Person、Address,或者实现细节可以是多个 +
。但是测试的逻辑是一样的。
非编码示例:
假设您正在构建一台机器,用于识别机场安检的电子设备的类型和品牌名称。机器通过处理它用相机看到的东西来做到这一点。
现在你的经理走进门,要求你对它进行单元测试。
然后,作为开发人员,您可以将 1000 个真实对象(如 MacBook Pro、Google Nexus、香蕉、iPad 等)放在它面前,并测试它们是否都能正常工作。
但您也可以使用模拟对象,例如外观相同的 MacBook Pro(没有真正的内部部件)或前面的塑料香蕉。您可以避免投资 1000 台真正的笔记本电脑和腐烂的香蕉。
关键是您不是要测试香蕉是否是假的。也不测试笔记本电脑是否是假的。您所做的只是测试您的机器是否会在看到香蕉时显示 not an electronic device
,而对于 MacBook Pro,它会显示:Laptop, Apple
。对于机器而言,其检测结果对于假冒/模拟的电子产品和真实的电子产品应该是相同的。如果您的机器还考虑了笔记本电脑(X 射线扫描)或香蕉的内部结构,那么您的模拟设备的内部结构也需要看起来相同。但您也可以使用不再工作的 MacBook。
如果您的机器测试了设备是否可以开机,那么您将需要真正的设备。
上面提到的逻辑也适用于实际代码的单元测试。这是一个函数应该与从真实输入(和交互)获得的真实值或在单元测试期间注入的模拟值相同。就像您如何避免使用真正的香蕉或 MacBook 一样,通过单元测试(和模拟),您可以避免做一些导致服务器返回 500、403、200 等状态代码的事情(强制您的服务器触发 500 仅在服务器关闭时触发,而 200 则在服务器启动时触发。
如果您必须在服务器上下切换之间不断等待 10 秒,那么运行 100 次以网络为中心的测试会变得很困难)。因此,您可以注入/模拟状态码为 500、200、403 等的响应,并使用注入/模拟的值测试您的单元/功能。
意识到:
有时您没有正确模拟实际对象。或者你不会嘲笑每一种可能性。例如,您的假笔记本电脑是黑色的,您的机器可以准确地与它们配合使用,但它不能与白色的假笔记本电脑配合使用。后来,当您将这台机器运送给客户时,他们抱怨说它并非一直工作。您会收到随机报告说它不起作用。您需要 3 个月的时间才能弄清楚假冒笔记本电脑的颜色需要更加多样化,以便您可以适当地测试您的模块。
对于一个真实的编码示例,对于状态代码 200(返回图像数据)与 200(未返回图像数据),您的实现可能会有所不同。出于这个原因,最好使用提供 code coverage 的 IDE,例如下图显示您的单元测试不会通过标有 brown 的行。
https://i.stack.imgur.com/ias4p.jpg
真实世界的编码示例:
假设您正在编写一个 iOS 应用程序并进行网络调用。您的工作是测试您的应用程序。测试/识别网络调用是否按预期工作不是您的责任。测试它是另一方(服务器团队)的责任。您必须删除此(网络)依赖项,并继续测试围绕它工作的所有代码。
网络调用可以通过 JSON 响应返回不同的状态代码 404、500、200、303 等。
您的应用程序应该适用于所有这些(如果出现错误,您的应用程序应该抛出预期的错误)。您使用模拟所做的是创建“虚拟的——类似于真实的”网络响应(例如带有 JSON 文件的 200 代码)并测试您的代码,而无需“进行真实的网络调用并等待您的网络响应”。您手动硬编码/返回所有类型的网络响应的网络响应,并查看您的应用程序是否按预期工作。 (您永远不会假设/测试 200 的数据不正确,因为这不是您的责任,您的责任是使用正确的 200 测试您的应用程序,或者在 400、500 的情况下,您测试您的应用程序是否抛出正确的错误)
这种创造想象的——类似于真实的被称为模拟。
为此,您不能使用原始代码(您的原始代码没有预先插入的响应,对吧?)。您必须向其中添加一些内容,注入/插入通常不需要的虚拟数据(或您班级的一部分)。
因此,您创建一个原始类的实例并添加您需要的任何内容(这里是网络 HTTPResponse、数据或在失败的情况下,您传递正确的 errorString、HTTPResponse),然后测试模拟类。
长话短说,模拟是为了简化和限制你正在测试的内容,同时让你满足一个类所依赖的内容。在这个例子中,你避免测试网络调用本身,而是通过模拟类来测试你的应用程序是否像你期望的那样使用注入的输出/响应工作
不用说,您分别测试每个网络响应。
现在我一直在想的一个问题是:合约/端点以及我的 API 的 JSON 响应会不断更新。我如何编写考虑到这一点的单元测试?
要详细说明这一点:假设模型需要一个名为 username
的键/字段。你测试这个并且你的测试通过了。 2 周后后端将密钥的名称更改为 id
。你的测试仍然通过。正确的?或不?
更新模拟是后端开发人员的责任吗?他们提供更新的模拟是否应该成为我们协议的一部分?
上述问题的答案是:单元测试+作为客户端开发人员的开发过程应该/将捕获过时的模拟响应。如果你问我怎么做?答案是:
如果不使用更新的 API,我们的实际应用程序将失败(或没有失败但没有所需的行为)......因此,如果失败......我们将对我们的开发代码进行更改。这再次导致我们的测试失败......我们必须纠正它。 (实际上,如果我们要正确地执行 TDD 过程,我们不会编写任何有关该字段的代码,除非我们为它编写测试......并且看到它失败然后去为它编写实际的开发代码。)
这一切都意味着后端不必说:“嘿,我们更新了模拟”......它最终通过您的代码开发/调试发生。 ّ因为这都是开发过程的一部分!虽然如果后端为您提供模拟响应,那么它会更容易。
我的全部观点是(如果您不能自动获取更新的模拟 API 响应)可能需要人工交互,即手动更新 JSON 并召开简短会议以确保其值是最新的,这将成为您流程的一部分
本节的编写感谢我们 CocoaHead 聚会小组中的闲散讨论
混乱:
我花了一段时间才没有混淆“类的单元测试”和“类的存根/模拟”。例如,在我们的代码库中,我们有:
类设备
类 DeviceTests
模拟设备类
类设备管理器
类 Device 是实际的类本身。
类 DeviceTests 是我们为 Device 类编写单元测试的地方
class MockDevice 是 Device 的模拟类。我们仅将其用于测试目的。例如,如果我们的 DeviceManager 需要进行单元测试,那么我们需要 Device 类的虚拟/模拟实例。 MockDevice 可用于满足虚拟/模拟实例的需要。
tldr 您使用模拟类/对象来测试其他对象。您不使用模拟对象来测试自己。
仅适用于 iOS 开发者:
这个 Practical Protocol-Oriented talk by Natasha Muraschev 就是一个很好的模拟示例。直接跳到 18:30 分钟,虽然幻灯片可能与实际视频不同步🤷♂️
我真的很喜欢成绩单中的这一部分:
因为这是测试...我们确实希望确保调用 Gettable 中的 get 函数,因为它可以返回,并且该函数理论上可以从任何地方分配一组食物。我们需要确保它被调用;
SO上有很多答案,网上有很多关于模拟的好帖子。您可能想要开始寻找的一个地方是 Martin Fowler Mocks Aren't Stubs 的帖子,他在其中讨论了许多嘲笑的想法。
在一段中 - 模拟是一种特殊的技术,允许在不依赖依赖关系的情况下测试代码单元。一般来说,模拟与其他方法的不同之处在于,用于替换代码依赖项的模拟对象将允许设置期望——模拟对象将知道您的代码如何调用它以及如何响应。
您最初的问题提到了 TypeMock,所以我在下面留下了我的答案:
TypeMock 是 commercial mocking framework 的名称。
它提供了 RhinoMocks 和 Moq 等免费模拟框架的所有功能,以及一些更强大的选项。
你是否需要 TypeMock 是值得商榷的——你可以使用免费的 mocking 库来做你想要的大多数 mocking,而且许多人认为 TypeMock 提供的功能通常会让你远离封装良好的设计。
正如另一个答案所说,“TypeMocking”实际上不是一个定义的概念,但可以理解为 TypeMock 提供的模拟类型,使用 CLR 分析器在运行时拦截 .Net 调用,从而提供更大的伪造对象的能力(不是要求例如需要接口或虚拟方法)。
Mock 是一种方法/对象,它以受控方式模拟真实方法/对象的行为。模拟对象用于单元测试。
通常,测试中的方法会调用其中的其他外部服务或方法。这些称为依赖项。一旦被模拟,依赖项的行为方式就是我们定义它们的方式。
由于依赖项由模拟控制,我们可以轻松地测试我们编码的方法的行为。这是单元测试。
What is the purpose of mock objects?
Unit tests vs Functional tests
模拟正在生成模拟真实对象行为以进行测试的伪对象
模拟类型的目的是切断依赖关系,以便将测试隔离到特定单元。存根是简单的代理,而模拟是可以验证使用情况的代理。模拟框架是一种可以帮助您生成存根和模拟的工具。
编辑:由于最初的措辞提到“类型模拟”,我觉得这与 TypeMock 有关。根据我的经验,一般术语只是“嘲笑”。请随意忽略以下关于 TypeMock 的信息。
TypeMock Isolator 与大多数其他模拟框架的不同之处在于它可以即时修改 IL。这允许它模拟大多数其他框架无法模拟的类型和实例。要使用其他框架模拟这些类型/实例,您必须提供自己的抽象并模拟它们。
TypeMock 以干净的运行时环境为代价提供了极大的灵活性。作为 TypeMock 实现其结果的方式的副作用,您有时会在使用 TypeMock 时得到非常奇怪的结果。
我认为 TypeMock 隔离器模拟框架的使用将是 TypeMocking。
它是一种生成用于单元测试的模拟的工具,无需考虑 IoC 编写代码。
如果您的模拟涉及网络请求,另一种选择是使用真正的测试服务器。您可以使用服务为您的测试生成请求和响应。
不定期副业成功案例分享