7

嘲讽的目的是什么?

我一直在关注一些使用 NUnit 进行测试和 Moq 进行模拟的 ASP.NET MVC 教程。不过,我对它的嘲笑部分有点不清楚。

4

8 回答 8

17

模拟的目的是将被测试的类与其他类隔离开来。

这在上课时很有帮助:

  • 连接到外部资源(文件系统、数据库、网络...)
  • 设置成本高,或尚不可用(正在开发的硬件)
  • 减慢单元测试的执行速度
  • 具有不确定的行为
  • 有(或者是)一个用户界面

它还使测试错误条件变得更容易,因为您构建模拟对象以便它返回和错误,抛出异常......

mock 可以记录它是如何被调用的(函数调用顺序、参数),这可以通过测试来验证。编辑:例如:您正在测试的方法发送一条消息,例如 IPC。mock对象的方法可以记录它被调用了多少次,他收到的参数(即要发送的消息)。然后测试可以询问模拟对象并断言发送的消息数量,消息的内容......同样,模拟对象可以记录在日志字符串中调用的方法,测试可以检索该字符串并断言在上面。

不要滥用模拟对象:测试行为而不是实现,否则单元测试将与代码过于紧密耦合,并且脆弱(重构时中断)。

模拟可以手动编码,也可以由模拟框架生成。

于 2009-06-16T15:46:45.163 回答
7

模拟允许您将被测类与其依赖项隔离开来。通常,您为被测类的每个依赖项创建一个模拟,并设置模拟以返回预期值。然后,您将模拟提供给您的测试类,而不是您的测试类所依赖的类的真实副本。然后,您可以使用模拟框架检查是否对模拟对象进行了预期的调用,以确保您的测试类正常运行。

于 2009-06-16T15:39:20.763 回答
4

它旨在从组集合中取笑单个实例。在不规则的对象聚会中使用了很多。

于 2009-06-16T15:55:17.250 回答
4

虽然模拟通常被理解为允许隔离被测类,但这不是模拟的重点(存根对此更好)。相反,我们需要看看当一个对象被告知做三件事之一时会发生什么。

  1. 直接输出 - 方法调用的结果
  2. 内部更改 - 在方法调用期间对类的更改
  3. 间接输出 - 被测代码调用不同的类

基于状态的测试完全是关于#1 和#2。#1 通过查看该方法给您的结果。#2 通过访问对象的内部状态。

这给我们留下了#3,我们可以采取两种途径。第一个是使用 Mock,第二个是使用 Test Spy。主要区别在于,在 Mock 上,您在执行被测代码之前创建期望值,然后让 mock 对其进行验证,而在 Test Spy 中,您执行被测代码,然后询问 Test Spy 是否发生了某些操作。

所以总结一下......当你考虑测试一个类的功能时,如果你需要测试间接输出(也就是对另一个类的调用),这就是 Mocking 发挥作用的地方。

于 2009-06-16T15:58:18.297 回答
3

我也是嘲笑的新手,但我会尝试一下。根据我的经验,模拟有两个主要好处:

  • 您可以在实际编写实现之前开始使用对象。您可以定义一个接口并使用模拟在单元测试甚至代码中使用该接口。
  • 模拟允许您在单元测试中隔离被测对象。使用模拟对象,您可以完全控制与被测对象交互的任何对象,从而从测试中删除外部依赖项。
于 2009-06-16T15:39:08.343 回答
3

“模拟”在测试和 TDD 圈子中是一个严重超载的术语。请参阅 Martin Fowler 的文章Mocks Aren't Stubs。一个“正确的”模拟知道它应该接收什么值,并在它没有得到预期的值时让你知道;这允许您进行交互测试而不是状态测试 - 您验证被测类是否以正确的顺序将正确的消息传递给其协作者。交互测试与传统的状态测试有很大不同,而且很难理解。请记住,交互测试是模拟的重点,这可能会使它们更容易理解。

于 2009-06-16T15:47:13.227 回答
0

另一个答案:

  • 存根 = 假对象能够在没有整个真实上下文的情况下运行您的测试

  • Mock = 假对象来记录你的组件的交互并验证这些交互

您可以在一个测试中拥有多个存根,但只有一个 mock,因为如果您有多个 mock,那么您肯定会测试多个功能(并且它违背了 test-one-thing 原则的目的)。

超越基础,Mocks 比传统的测试状态验证更多的是行为验证,传统的状态验证是在对组件进行操作后检查组件的状态(Arrange、Act、Assert with Mocks,它更像是 Arrange、Act、Verify):

状态验证 Assert.AreEqual(valueExpected,mycomponent.Property); 行为验证:myMock.WasCalled(MyMethod);

于 2009-06-23T00:56:18.857 回答
0

伟大的实时模拟示例Bert F

单元测试

想象一下这个系统的单元测试:

cook <- waiter <- customer

通常很容易设想测试一个低级组件,例如cook

cook <- test driver

测试司机只需点不同的菜,并验证厨师为每个订单返回正确的菜。

更难测试一个中间组件,比如服务员,它利用了其他组件的行为。一个天真的测试人员可能会像测试厨师组件一样测试服务员组件:

cook <- waiter <- test driver

试驾员会点不同的菜,并确保服务员返回正确的菜。不幸的是,这意味着对服务员组件的测试可能依赖于厨师组件的正确行为。如果 cook 组件具有任何不利于测试的特性,例如不确定性行为(菜单包括厨师作为一道菜的惊喜)、大量依赖项(厨师不会在没有他的全部员工的情况下做饭)或大量资源(有些菜肴需要昂贵的食材或需要一个小时才能烹饪)。

由于这是一个服务员测试,理想情况下,我们只想测试服务员,而不是厨师。具体来说,我们要确保服务员正确地将客户的订单传达给厨师,并将厨师的食物正确地交付给客户。

单元测试意味着独立地测试单元,因此更好的方法是使用Fowler 所说的测试替身(假人、存根、假货、模拟)来隔离被测组件(服务员) 。

    -----------------------
   |                       |
   v                       |
test cook <- waiter <- test driver

在这里,测试厨师与测试驾驶员“勾结”。理想情况下,被测系统的设计使得测试厨师可以很容易地被替换(注入)以与服务员一起工作,而无需更改生产代码(例如,无需更改服务员代码)。

模拟对象

现在,测试厨师(测试替身)可以通过不同的方式实现:

  • 假厨师 - 使用冷冻晚餐和微波炉假装厨师的人,
  • 存根厨师 - 无论您点什么,热狗供应商都会给您热狗,或者
  • 模拟厨师 - 一名卧底警察在执行脚本后假装自己是一名厨师进行刺痛行动。

有关 fakes vs stubs vs mocks vs dummies 的更多细节,请参阅Fowler 的文章,但现在,让我们专注于模拟厨师。

    -----------------------
   |                       |
   v                       |
mock cook <- waiter <- test driver

对服务员组件进行单元测试的很大一部分集中在服务员与厨师组件的交互方式上。基于模拟的方法侧重于完全指定正确的交互是什么,并检测何时出错。

模拟对象预先知道在测试期间应该发生什么(例如,将调用它的哪些方法调用等),并且模拟对象知道它应该如何反应(例如,要提供什么返回值)。模拟将表明真正发生的事情是否与应该发生的事情不同。自定义模拟对象可以针对每个测试用例的预期行为进行编码,但是模拟框架努力允许在测试用例中直接清楚、轻松地指示这种行为规范。

围绕基于模拟的测试的对话可能如下所示:

试驾员模拟厨师期待一份热狗订单并给他这个假热狗作为回应

测试司机(冒充顾客)给服务员我想要一个热狗,请服务员
模拟厨师1 个热狗,请
模拟厨师服务员订购:准备好 1 个热狗(给服务员假热狗)
服务员测试司机这是你的热狗(给测试司机假热狗)

测试驱动程序:测试成功!

但是由于我们的服务员是新来的,所以可能会发生这种情况:

试驾员模拟厨师期待一份热狗订单并给他这个假热狗作为回应

测试司机(冒充顾客)对服务员我想要一个热狗,请服务员
模拟厨师1 个汉堡包,请
模拟厨师停止测试:我被告知期待热狗订单!

测试驱动程序注意到问题:测试失败!- 服务员改变了订单

或者

试驾员模拟厨师期待一份热狗订单并给他这个假热狗作为回应

测试司机(冒充顾客)给服务员我想要一个热狗,请服务员
模拟厨师1 个热狗,请
模拟厨师服务员订购:准备好 1 个热狗(给服务员假热狗)
服务员测试司机:这是你的炸薯条(给其他订单的炸薯条给测试司机)

试驾员注意到意料之外的炸薯条:测试失败!服务员回错菜了

如果没有基于存根的对比示例,可能很难清楚地看到模拟对象和存根之间的区别,但是这个答案已经太长了:-)

另请注意,这是一个非常简单的示例,并且模拟框架允许对组件的预期行为进行一些非常复杂的规范,以支持全面的测试。有大量关于模拟对象和模拟框架的材料以获取更多信息。

于 2014-04-04T11:53:29.147 回答