什么是模拟?


Answers:


598

序言:如果您在词典中查找名词模拟,您会发现该词的定义之一是模仿的东西


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

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


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

模拟就像存根一样,但是测试还将验证被测对象是否按预期调用了模拟。测试的一部分是验证是否正确使用了模拟。

举个例子:您可以通过实现一个简单的内存结构来存储记录来对数据库进行存根。然后,被测对象可以将记录读写到数据库存根中,以允许其执行测试。这可以测试与数据库无关的对象的某些行为,而数据库存根只是为了让测试运行而包括在内。

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


18
这是一个很好的答案,但是不必要地将模拟的概念限制为对象。用“单位”代替“对象”会使它更通用。
罗杰里奥(Rogério)2010年

1
我确实了解存根与模拟的区别。唯一的事情是,如果您正在使用存根测试您的案件并且通过了,那么您是否不能得出结论,您已经在使用存根,因此您不再需要验证
亲爱的

91

其他答案解释了什么是嘲笑。让我通过不同的示例向您介绍。相信我,这实际上比您想象的要简单得多。

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是否为整数。我没有断言。

我只是测试看看LineB(我的实现)是否给出了模拟值,1并且5是否按我的预期进行。

显然,用真实的话来说,这可能变得更加复杂。参数可以是自定义对象,例如Person,Address,或者实现详细信息可以不止一个+。但是测试的逻辑是相同的。

非编码示例:

假设您要制造一台用于识别机场安全电子设备类型和品牌的机器。机器通过处理其相机看到的图像来完成此操作。

现在,您的经理走进门,请您对它进行单元测试。

然后,作为开发人员,您可以在其前面携带1000个真实对象,例如MacBook Pro,Google Nexus,香蕉,iPad等,然后进行测试并查看它们是否全部正常。

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

关键是您不打算测试香蕉是否是假的。也不测试笔记本电脑是否是假的。您要做的就是测试您的机器是否一旦看到香蕉就会说出来,not an electronic device而对于MacBook Pro会说:Laptop, Apple。对于机器,其检测结果对于假/模拟电子产品和真实电子产品应相同

上面提到的逻辑同样适用于实际代码的单元测试。那是一个函数应该与您从真实输入(和交互)获得或模拟的真实值相同您在单元测试期间注入的值。就像您通过使用真实的香蕉或MacBook来节省自己的成本一样,通过进行单元测试(和模拟),您不必进行某些操作即可导致服务器返回状态码500、403、200等(强制您的服务器仅在服务器关闭时才触发500,而在服务器启动时才触发200。如果您必须在服务器上下切换之间持续等待10秒,则很难运行100个针对网络的测试。因此,您可以注入/模拟状态代码为500、200、403等的响应,并使用注入/模拟的值来测试您的单元/功能。

编码示例:

假设您正在编写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聚会小组中的闲聊


仅适用于iOS开发者:

Natasha Muraschev进行的面向实践协议的演讲是一个很好的模拟例子。只需跳至18:30分钟,尽管幻灯片可能与实际视频不同步🤷‍♂️

我真的很喜欢笔录的这一部分:

由于正在测试...我们确实要确保get从中Gettable调用该函数,因为它可以返回,并且该函数理论上可以从任何地方分配食品数组。我们需要确保它被调用;


3
很棒的例子,我只想补充一点,在这个特定的例子中,子类充当了模拟,但这个例子也使用了存根。硬编码的JSON响应被视为存根响应。我之所以仅添加它是因为很难区分模拟和存根,但是此示例清楚地说明了如何将它们一起使用。
user3344977

很好的解释,谢谢。关于API更改问题的一小部分调整。如果这不是您的API,那么您不属于开发流程怎么办?我想知道客户端库何时发生故障。
ThinkDigital

@ThinkDigital良好的API提供程序具有良好的发行说明,并且可以正确传达更改,如果您没有该频道,那么也许是时候参加会议并进行讨论了。优秀的开发人员将始终关注新版本的API更改,并避免仅升级API版本。您有API版本吗?如果这些都没有发现,那么在进行质量检查时,您会找到答案,然后更新测试←整个团队的职责。→对单个开发人员负责:应该不在乎。只需处理服务器返回错误的情况,或服务器不返回错误但无法解析json的情况,或处理正确的情况。
亲爱的

感谢您的回复,@ Honey!就我而言,我正在维护pub.dev的客户端,该客户端具有API,但严重缺乏。如此之多,以至于通过抓取其网站来制作API比使用其官方API更好。因此,对站点的更改可能会破坏代码,并且在这种情况下,他们无需费心更新任何人。该网站是开源的,但是维护基于更琐碎的更改的API则是另一回事。
ThinkDigital

32

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

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


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

TypeMock是商业模拟框架的名称。

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

无论您是否需要TypeMock都是有争议的-您可以使用免费的模拟库来完成您想做的大多数模拟,而且许多人认为TypeMock所提供的功能通常会使您脱离封装良好的设计。

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


@Masoud从未提及TypeMock。他的问题通常是关于“类型嘲笑”的。
Peter Lillevold

4
@Peter-如另一条评论所述,检查问题的编辑历史记录。如果发布答案,然后原始问题已完全更改,我将无能为力。
大卫·霍尔

9

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

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

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

模拟对象的目的是什么?

嘲笑与存根

单元测试与功能测试


7

模拟正在生成模拟真实对象行为以进行测试的伪对象


5

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

编辑:自从原始措辞提到“类型模拟”后,我得到的印象是这与TypeMock有关。以我的经验,通用术语只是“模拟”。请随时忽略以下有关TypeMock的信息。

TypeMock Isolator与大多数其他模拟框架的不同之处在于,它可以动态运行修改的IL。这样就可以模拟大多数其他框架无法模拟的类型和实例。要与其他框架一起模拟这些类型/实例,您必须提供自己的抽象并对其进行模拟。

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


@Masoud从未提及TypeMock。他的问题通常是关于“类型嘲笑”的。
Peter Lillevold

1
@Peter:最初的措辞是“什么是类型嘲笑?”。
Brian Rasmussen 2010年

我知道。由于“类型嘲笑”不等同于“ TypeMock”,因此我发现您的回答和@Oded回答都不太合理。
Peter Lillevold

1
@Peter:根据我的经验,通用术语是“嘲笑”,但是无论如何我都更新了答案,希望能使这一点变得清楚。感谢您的输入。
Brian Rasmussen 2010年

3

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

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


@Masoud从未提及TypeMock。他的问题通常是关于“类型嘲笑”的。
Peter Lillevold

3
实际上,最初的问题在“模拟”之前包括“类型”一词,但后来被删除了。这就是为什么某些答案包含有关TypeMock的特定信息的原因。
Martin Liversage 2010年

1

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


我只是尝试访问它,所以花了几分钟。谁说它也不是秘密记录请求?最后,作为注释可能会更好:)
Kieren Johnstone

实际上,我将其删除了,因为我不想将其移至免费托管。是的,这应该是一条评论。它是开源的,因此如果担心记录请求,则可以自己运行。github.com/captainchung/TesterUrl
马修·钟 Matthew Chung)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.