模拟和存根有什么区别?

我已经阅读了很多关于测试中的mocking vs stack的文章,包括马丁·福勒的嘲笑不是存根,但仍然不明白其中的区别。

513284 次浏览

模拟只是测试行为,确保调用某些方法。存根是特定对象的可测试版本(本身)。

你说的苹果方式是什么意思?

存根是一个简单的假对象。它只是确保测试顺利运行。
模拟是一个更智能的存根。您验证您的测试通过它。

以下是我的理解…

  • 如果您在本地创建测试对象并为本地服务提供该对象,则您正在使用模拟对象。这将测试您在本地服务中实现的方法。它用于验证行为

  • 当您从真正的服务提供者那里获得测试数据时,虽然是从接口的测试版本获得对象的测试版本,但您正在使用存根存根可以有逻辑来接受某些输入并给出相应的输出,以帮助您执行状态验证…

存根

我认为最大的区别是你已经用预定行为编写的存根。因此,你将有一个实现了你为测试目的伪造的依赖项(最有可能是抽象类或接口)的类,并且方法将只是用集合响应存根。它们不会做任何花哨的事情,你已经在测试之外为它编写了存根代码。

Mock

模拟是作为测试的一部分,你必须按照你的期望进行设置的东西。模拟不是以预定的方式设置的,所以你有在测试中执行它的代码。模拟在某种程度上是在运行时确定的,因为设置期望的代码必须在执行任何操作之前运行。

Mocks和Stubs的区别

使用模拟编写的测试通常遵循initialize -> set expectations -> exercise -> verify模式进行测试。而预先编写的存根将遵循initialize -> exercise -> verify

Mocks和Stubs之间的相似性

两者的目的都是为了消除测试类或函数的所有依赖关系,因此您的测试在试图证明的内容上更集中、更简单。

存根不会通过你的测试,模拟可以。

codeschool.com课程僵尸的Rails测试中,他们给出了术语的定义:

存根

用于用返回指定结果的代码替换方法。

Mock

具有调用该方法的断言的存根。

因此,正如肖恩·哥本哈根在他的回答中所描述的那样,不同之处在于嘲笑设定了期望(即断言,关于他们是否或如何被调用)。

我认为他们之间最重要的区别是他们的意图。

让我试着解释一下为什么存根 vs.为什么模拟

假设我正在为我的mac twitter客户端的公共时间线控制器编写测试代码

这是测试示例代码

twitter_api.stub(:public_timeline).and_return(public_timeline_array)client_ui.should_receive(:insert_timeline_above).with(public_timeline_array)controller.refresh_public_timeline
  • STUB:到twitter API的网络连接非常慢,这使我的测试很慢。我知道它会返回时间线,所以我做了一个模拟HTTP twitter API的存根,这样我的测试就会运行得非常快,即使我离线也可以运行测试。
  • MOCK:我还没有编写任何UI方法,我不确定我需要为我的用户界面对象编写什么方法。我希望通过编写测试代码来知道我的控制器将如何与我的用户界面对象协作。

通过编写mock,您可以通过验证期望是否得到满足来发现对象的协作关系,而存根仅模拟对象的行为。

如果你想了解更多关于模拟的信息,我建议阅读这篇文章:http://jmock.org/oopsla2004.pdf

存根用于您在测试中设置的理论收益值的方法。Mocks用于在断言中验证它们被调用的空方法。

前言

对象有几种定义,它们不是真实的。一般术语是测试双。这个术语包括:假人假的存根模拟

参考

根据Martin Fowler的文章

  • 假人对象被传递但从未实际使用过。通常它们只是用于填充参数列表。
  • 对象实际上有工作实现,但通常会采取一些捷径,这使得它们不适合生产(内存数据库就是一个很好的例子)。
  • 存根为测试期间的调用提供预置答案,通常根本不响应为测试编程的内容之外的任何内容。存根还可以记录有关调用的信息,例如记住它“发送”的消息的电子邮件网关存根,或者可能只记住它“发送”的消息数量。
  • 嘲笑是我们在这里谈论的:对象预先编程了期望,这些期望形成了它们期望接收的调用的规范。

风格

Mocks vs Stubs=行为测试vs状态测试

原理

根据每次测试只测试一件事的原理,一个测试中可能有几个存根,但通常只有一个模拟。

生命周期

使用存根测试生命周期:

  1. 设置-准备正在测试的对象及其存根协作者。
  2. 练习-测试功能。
  3. 验证状态-使用断言检查对象的状态。
  4. 拆解-清理资源。

使用模拟测试生命周期:

  1. 设置数据-准备正在测试的对象。
  2. 设置期望-在主对象使用的模拟中准备期望。
  3. 练习-测试功能。
  4. 验证期望-验证是否在模拟中调用了正确的方法。
  5. 验证状态-使用断言检查对象的状态。
  6. 拆解-清理资源。

总结

模拟和存根测试都给出了问题的答案:结果如何?

使用模拟测试也感兴趣:结果是如何实现的?

如果将其与调试进行比较:

存根就像确保一个方法返回正确的值

Mock实际上就像步入方法,在返回正确的值之前确保里面的所有内容都是正确的。

存根帮助我们运行测试。如何?它提供了有助于运行测试的值。这些值本身不是真实的,我们创建这些值只是为了运行测试。例如,我们创建一个HashMap来为我们提供与数据库表中的值相似的值。所以我们不是直接与数据库交互,而是与Hashmap交互。

Mock是一个假对象,它运行测试。我们把断言放在哪里。

请参阅下面使用C#和Moq框架的模拟与存根示例。Moq没有Stub的特殊关键字,但您也可以使用Mock对象来创建存根。

namespace UnitTestProject2{using Microsoft.VisualStudio.TestTools.UnitTesting;using Moq;[TestClass]public class UnitTest1{/// <summary>/// Test using Mock to Verify that GetNameWithPrefix method calls Repository GetName method "once" when Id is greater than Zero/// </summary>[TestMethod]public void GetNameWithPrefix_IdIsTwelve_GetNameCalledOnce(){// Arrangevar mockEntityRepository = new Mock<IEntityRepository>();mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));
var entity = new EntityClass(mockEntityRepository.Object);// Actvar name = entity.GetNameWithPrefix(12);// AssertmockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Once);}/// <summary>/// Test using Mock to Verify that GetNameWithPrefix method doesn't call Repository GetName method when Id is Zero/// </summary>[TestMethod]public void GetNameWithPrefix_IdIsZero_GetNameNeverCalled(){// Arrangevar mockEntityRepository = new Mock<IEntityRepository>();mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));var entity = new EntityClass(mockEntityRepository.Object);// Actvar name = entity.GetNameWithPrefix(0);// AssertmockEntityRepository.Verify(m => m.GetName(It.IsAny<int>()), Times.Never);}/// <summary>/// Test using Stub to Verify that GetNameWithPrefix method returns Name with a Prefix/// </summary>[TestMethod]public void GetNameWithPrefix_IdIsTwelve_ReturnsNameWithPrefix(){// Arrangevar stubEntityRepository = new Mock<IEntityRepository>();stubEntityRepository.Setup(m => m.GetName(It.IsAny<int>())).Returns("Stub");const string EXPECTED_NAME_WITH_PREFIX = "Mr. Stub";var entity = new EntityClass(stubEntityRepository.Object);// Actvar name = entity.GetNameWithPrefix(12);// AssertAssert.AreEqual(EXPECTED_NAME_WITH_PREFIX, name);}}public class EntityClass{private IEntityRepository _entityRepository;public EntityClass(IEntityRepository entityRepository){this._entityRepository = entityRepository;}public string Name { get; set; }public string GetNameWithPrefix(int id){string name = string.Empty;if (id > 0){name = this._entityRepository.GetName(id);}return "Mr. " + name;}}public interface IEntityRepository{string GetName(int id);}public class EntityRepository:IEntityRepository{public string GetName(int id){// Code to connect to DB and get name based on Idreturn "NameFromDb";}}}

A假的是一个通用术语,可用于描述存根或模拟对象(手写或其他方式),因为它们看起来都像

假的是存根还是模拟取决于它在实际对象中的使用方式当前测试。如果它用于检查交互(断言反对),它是模拟对象。否则,它是一个存根。

假货确保测试顺利运行。这意味着你未来测试的读者将了解假对象的行为,而无需阅读其源代码(无需依赖外部资源)。

测试顺利运行是什么意思?
例如下面的代码:

 public void Analyze(string filename){if(filename.Length<8){try{errorService.LogError("long file entered named:" + filename);}catch (Exception e){mailService.SendEMail("admin@hotmail.com", "ErrorOnWebService", "someerror");}}}

你想测试邮件服务。发送电子邮件()方法,为此你需要在测试方法中模拟一个Exception,所以你只需要创建一个Fake Stub error Service类来模拟该结果,然后你的测试代码将能够测试mail Service. SendEMail()方法。正如你所看到的,你需要模拟一个来自另一个外部依赖错误服务类的结果。

阅读上面的所有解释,让我试着浓缩一下:

  • 存根:让测试运行的虚拟代码,但您不关心它会发生什么。替代真实的工作代码。
  • Mock:作为测试的一部分,您审核被正确调用的虚拟代码。替代真实的工作代码。
  • 间谍:拦截和验证一些呼叫到真实工作代码的虚拟代码,避免了替换所有真实代码的需要。

下面是对每一个的描述,然后是真实世界的样本。

  • 假人-只有虚假值来满足API

    示例:如果您正在测试一个类的方法,该方法需要在测试中没有效果的构造函数中使用许多强制参数,那么您可以创建虚拟对象以创建类的新实例。

  • -创建一个类的测试实现,该类可能依赖于某些外部基础设施。(您的单元测试不是实际上与外部基础设施交互是一种很好的做法。)

    示例:创建用于访问数据库的假实现,将其替换为in-memory集合。

  • 存根-覆盖返回硬编码值的方法,也称为state-based

    示例:您的测试类依赖于方法Calculate()需要5分钟才能完成。与其等待5分钟,您可以将其实际实现替换为返回硬编码值的存根;只需一小部分时间。

  • Mock-与Stub非常相似,但interaction-based不是基于状态的。这意味着您不期望Mock返回一些值,而是假设进行了特定的方法调用顺序。

    示例:您正在测试一个用户注册类。调用Save后,它应该调用SendConfirmationEmail

StubsMocks实际上是Mock的子类型,它们都将实际实现与测试实现交换,但出于不同的特定原因。

我在回答中使用了python示例来说明差异。

存根-存根是一种软件开发技术,用于在开发生命周期的早期实现类的方法。它们通常用作已知接口实现的占位符,其中接口已最终确定或已知,但实现尚未已知或最终确定。你从存根开始,这仅仅意味着你只写下函数的定义,并将实际代码留给以后。优点是你不会忘记方法,你可以在代码中看到它时继续思考你的设计。你也可以让存根返回静态响应,这样响应就可以立即被代码的其他部分使用。存根对象提供有效的响应,但无论你传入什么输入,它都是静态的,你总是会得到相同的响应:

class Foo(object):def bar1(self):pass
def bar2(self):#or ...raise NotImplementedError
def bar3(self):#or return dummy datareturn "Dummy Data"

Mock对象用于模拟测试用例,它们验证这些对象是否调用了某些方法。模拟对象是模拟对象,以受控的方式模拟真实对象的行为。您通常会创建一个模拟对象来测试其他对象的行为。模拟让我们模拟那些不可用或对单元测试来说太笨拙的资源。

mymodule.py:

import osimport os.path
def rm(filename):if os.path.isfile(filename):os.remove(filename)

test.py:

from mymodule import rmimport mockimport unittest
class RmTestCase(unittest.TestCase):@mock.patch('mymodule.os')def test_rm(self, mock_os):rm("any path")# test that rm called os.remove with the right parametersmock_os.remove.assert_called_with("any path")
if __name__ == '__main__':unittest.main()

这是一个非常基本的示例,它只运行rm并断言调用它的参数。您可以将mock与对象一起使用,而不仅仅是这里所示的函数,您还可以返回一个值,以便模拟对象可以用来替换存根进行测试。

更多关于unittest.mock的信息,请注意python 2. x mock不包含在unittest中,而是一个可下载的模块,可以通过pip(pip install mock)下载。

我还读过Roy Osherove的《单元测试的艺术》,我认为如果有一本类似的书使用Python和Python示例编写就太好了。如果有人知道这样的书,请分享。干杯:)

一个存根是一个实现组件接口的对象,但是可以将存根配置为返回适合测试的值,而不是返回组件在调用时将返回的值。使用存根,单元测试可以测试一个单元是否可以处理来自其协作者的各种返回值。在单元测试中使用存根而不是真正的协作者可以这样表示:

单元测试-->存根

单元测试-->单元-->存根

单元测试断言结果和单元状态

首先单元测试创建存根并配置其返回值。然后单元测试创建单元并在其上设置存根。现在单元测试调用单元,然后单元测试调用存根。最后单元测试对单元上的方法调用的结果进行断言。

一个模拟就像一个存根,只是它也有一些方法,可以确定在Mock上调用了哪些方法.因此,使用模拟既可以测试单元是否可以正确处理各种返回值,也可以测试单元是否正确使用合作者。例如,你无法通过从道对象返回的值看到数据是使用语句还是准备语句从数据库中读取的。你也无法看到在返回值之前是否调用了connection.close()方法。这在模拟中是可能的。换句话说,模拟使得测试单元与合作者的完整交互成为可能。不仅仅是返回单元使用的值的合作者方法。在单元测试中使用模拟可以这样表达:

单元测试-->mock

单元测试-->单元-->模拟

单元测试断言单元的结果和状态

单元测试断言mock上调用的方法

更多详情>>这里

存根是一个空函数,用于在测试期间避免未处理的异常:

function foo(){}

mock是一种人工函数,用于在测试期间避免操作系统、环境或硬件依赖:

function foo(bar){ window = this; return window.toString(bar); }

在断言和状态方面:

  • 在事件或状态更改之前断言Mocks
  • 不断言存根,它们在事件之前提供状态以避免执行来自不相关单元的代码
  • 间谍像存根一样设置,然后在事件或状态更改后断言
  • 不断言假货,它们在具有硬编码依赖关系的事件之后运行以避免状态

参考文献

要非常明确和实际:

存根:一个类或对象,它实现了要伪造的类/对象的方法,并始终返回您想要的内容。

JavaScript中的示例:

var Stub = {method_a: function(param_a, param_b){return 'This is an static result';}}

模拟:与存根相同,但它添加了一些在调用方法时“验证”的逻辑,因此您可以确定某些实现正在调用该方法。

正如@mLevan所说,假设您正在测试一个用户注册类。调用Save后,它应该调用SendConfimationEmail。

一个非常愚蠢的代码示例:

var Mock = {calls: {method_a: 0}
method_a: function(param_a, param_b){this.method_a++;console.log('Mock.method_a its been called!');}}

我认为这个问题最简单、最清晰的答案来自他的书单元测试的艺术中的Roy Osherove(第85页)

判断我们正在处理存根的最简单方法是注意存根永远不会测试失败。测试使用的断言总是反对被测试的类。

另一方面,测试将使用模拟对象来验证是否测试失败与否[…]

同样,模拟对象是我们用来查看测试是否失败的对象。

存根和模拟都是假货。

如果你对假货进行断言,这意味着你正在使用假货作为模拟,如果你使用假货只是为了运行测试而没有对它进行断言,那么你正在使用假货作为存根。

来自jMock开发人员的论文模拟角色,而不是对象

存根是生产代码的虚拟实现,返回罐装模拟对象充当存根,但也包括断言测量目标物体与其邻居的相互作用。

所以,主要的区别是:

  • 在存根上设置的期望通常是通用的,而在模拟上设置的期望可以更“聪明”(例如,在第一次调用时返回this,在第二次调用时返回this等)。
  • 存根主要用于设置SUT的间接输入,而嘲笑可用于测试SUT的间接投入和间接输出。

综上所述,同时也试图从标题福勒的文章驱散混乱:模拟是存根,但它们不仅仅是存根

我喜欢Roy Osherove [视频链接]的解释。

创建的每个类或对象都是假的。如果您验证,它是一个模拟

否则就是存根

存根是为测试目的构建的假对象。模拟是记录预期调用是否有效发生的存根。

我偶然发现了UncleBob小嘲笑者写的这篇有趣的文章。它以一种非常容易理解的方式解释了所有的术语,所以对初学者很有用。马丁·福勒的文章很难读,尤其是对像我这样的初学者来说。

这张幻灯片很好地解释了主要差异。

在此处输入图片描述

*来自CSE 403讲座16,华盛顿大学(由“Marty Stepp”创建的幻灯片)

Mock-mock拦截对方法或函数(或一组方法和函数,如模拟类的情况)的调用。它不是该方法或函数的替代方案。在该拦截中,mock可以做任何它想做的事情,例如记录输入和输出、决定短路调用、更改返回值等。

存根-存根是一个方法或函数(或一组方法和函数,就像存根类的情况一样)的有效完整工作实现,它与它正在存根的方法、函数或一组方法和函数具有相同的接口/签名。存根实现通常只会做在单元测试上下文中可以接受的事情,这意味着例如,它不会做IO,同时模仿它正在存根的东西的行为。

他使用的通用术语是测试双倍(想想特技替身)。Test Double是一个通用术语,适用于您出于测试目的替换生产对象的任何情况。Gerard列出了各种替身:

  • 假人对象被传递但从未实际使用过。通常它们只是用于填充参数列表。
  • 对象实际上有工作实现,但通常会采取一些快捷方式,这使得它们不适合生产(InMemoryTestDatabase是一个很好的例子)。
  • 存根提供测试期间调用的预制答案,通常根本不响应为测试编程的任何内容。
  • 间谍是根据调用方式记录一些信息的存根。其中一种形式可能是记录发送了多少条消息的电子邮件服务(也称为部分模拟)。
  • 嘲笑被预先编程了期望,这些期望形成了他们期望接收的呼叫的规范。如果他们收到了他们不期望的呼叫,他们可以抛出异常,并在验证过程中进行检查,以确保他们得到了他们期望的所有呼叫。

来源

上面有很多有效的答案,但我认为值得一提的是鲍勃叔叔:https://8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html

有史以来最好的解释和例子!

  • Stubs vs. Mocks
    • 存根
      1. 为方法调用提供特定答案
        • 例如:myStubbedService.get值()只返回被测代码所需的字符串
      2. 被测试代码用来隔离它
      3. 测试失败
        • 例如:myStubbedService.get值()只返回存根值
      4. 经常实现抽象方法
    • 嘲笑
      1. 存根的"超集";可以断言某些方法被调用
        • 例如:验证myMockedService.get值()只被调用一次
      2. 用于测试被测代码的行为
      3. 测试失败
        • 验证myMockedService.get值()被调用一次;验证失败,因为我测试的代码没有调用myMockedService.get值()
      4. 经常模拟接口

让我们看看双打测试:

  • :假货是具有工作实现的对象,但与生产实现不同。例如:数据访问对象或存储库的内存实现。
  • Stub:Stub是一个保存预定义数据并在测试期间使用它来接听调用的对象。例如:需要从数据库中抓取一些数据以响应方法调用的对象。

  • 嘲笑:Mocks是注册它们接收到的调用的对象。在测试断言中,我们可以在Mocks上验证是否执行了所有预期的操作。例如:调用电子邮件发送服务的功能。 有关更多信息,请检查这个

测试对象对某些提示(函数调用)或其他刺激执行操作。以下是测试情况的具体示例。

情景----EMT学生考试

一名学生已经学习成为一名紧急医疗技术员。如果你不熟悉这种测试情况,请观看第0季第10集的伊恩·加拉格尔。

为了测试目的,寻找患有各种疾病的病人太贵了。相反,我们使用演员。我们问测试对象(伊恩)“你到达现场,病人被固定下来,失去知觉,你首先做什么?”伊恩回答说“我检查现场是否安全”。测试教练说“现场是安全的”。

教师(和演员)能够为测试对象的查询注入任意答案。

在这里,教练(和演员)是一个模拟。医学培训使用与计算机科学家相同的术语(例如模拟代码模拟)。

场景-注册网站

您正在测试您听说过的新电子邮件服务Yahoo。为了注册,您必须提供您的生日和其他侵入性问题的答案。

该网站要求您年满21岁。因此您输入1970年1月1日的值。它符合要求,并使您免于实施记住我的生日并输入它的工作流程的费力过程。

这个日期是一个存根。这个词的用法是特定于计算机科学的。

使用心智模型真的帮助我理解了这一点,而不是所有的解释和文章,这并没有完全“沉入”。

想象一下,你的孩子在桌子上有一个玻璃板,他开始玩它。现在,你担心它会破碎。所以,你给他一个塑料板。这将是一个Mock(相同的行为,相同的界面,“更软”的实现)。

现在,假设你没有塑料替代品,所以你解释“如果你继续玩它,它会坏的!”。这是一个存根,你提前提供了一个预定义的状态。

假人将是他甚至没有使用的分叉……而间谍可能是提供与您已经使用的相同的解释。

假设您有一个名为员工服务的类要测试,并且它对名为员工道的接口有一个依赖项:

public class EmployeeService{private EmployeeDao dao;public EmployeeService(Dao dao){this.dao = dao;}
public String getEmployeeName(int id){Employee emp = bar.goToDatabaseAndBringTheEmployeeWithId(id);return emp != null?emp.getFullName:null;}//Further state and behavior}
public interface EmployeeDao{Employee goToDatabaseAndBringTheEmployeeWithId(int id);}

在您的测试类中:

public class EmployeeServiceTest{EmployeeService service;EmployeeDao mockDao = Mockito.mock(EmployeeDao.class);//Line 3
@Beforepublic void setUp(){service = new EmployeeService(mockDao);}//Tests//....}

在上面的测试类的第3行中,我们对模拟框架(在本例中是Mockito)说:“嘿,Mockito,给我制作一个具有员工道功能的对象。”框架将创建一个具有方法goToDatabaseAndBringTheEmployeeWithId但实际上没有主体的对象。你的工作是指导模拟做什么。这是一个模拟。

但是,你也可以创建一个类来实现员工道接口,并在测试类中使用它:

public EmployeeDaoStub implements EmployeeDao{public Employee goToDatabaseAndBringTheEmployeeWithId(int id){//No trip to DB, just returning a dummy Employee objectreturn new Employee("John","Woo","123 Lincoln str");}}

在你的测试类中,这次使用存根而不是模拟:

public class EmployeeServiceTest{EmployeeService service;EmployeeDao daoStub = new EmployeeDaoStub();//Line 3
@Beforepublic void setUp(){service = new EmployeeService(daoStub);}//Tests//....}

所以总而言之,存根是你创建的(或其他人创建的)类,专门为了模仿一些依赖关系,只是为了拥有所需的状态。是的,正如所有其他人所说,它主要是关于一个状态。而模拟通常是由模拟框架创建的,你不知道它的内部是什么样子。但是有了存根,你知道你将得到什么类:它就是你创建的那个。

哦,顺便说一句,如果您的依赖项是一个类而不是一个接口,您可以扩展该类来创建您的存根。

Stubs和Mocks都覆盖了外部依赖项,但区别在于

存根->测试数据

嘲笑->测试行为


假货/假货->什么都不要测试(只需用空方法覆盖功能,例如替换Logger以避免测试时的任何日志噪声)

一个mock既是一个技术和功能对象。

模拟是技术性的。它确实是由一个模拟库(EasyMock、JMockit和最近的Mockito以这些而闻名)创建的,这要归功于字节码生成
模拟实现是产生,在某种程度上,我们可以仪器它在调用方法时返回一个特定的值,但也有一些其他的事情,例如验证模拟方法是否使用一些特定的参数(严格检查)或任何参数(没有严格检查)来调用。

实例化一个mock:

@Mock Foo fooMock

记录一个行为:

when(fooMock.hello()).thenReturn("hello you!");

验证调用:

verify(fooMock).hello()

这些显然不是实例化/覆盖Foo类/行为的自然方式。这就是为什么我提到技术方面。

但是这个mock也很实用,因为它是我们需要与SUT隔离的类的实例。并且上面记录了行为,我们可以在SUT中以与存根相同的方式使用它。


存根是只是一个功能对象:这是我们需要与SUT隔离的类的实例,仅此而已。这意味着在我们的单元测试期间需要的存根类和所有行为装置都必须明确定义。
例如,存根hello()需要子类Foo类(或实现它的接口)并覆盖hello()

public class HelloStub extends Hello{public String hello {return "hello you!";}}

如果另一个测试场景需要另一个值返回,我们可能需要定义一种通用的方法来设置返回:

public class HelloStub extends Hello{public HelloStub(String helloReturn){this.helloReturn = helloReturn;}public String hello {return helloReturn;}}

其他情况:如果我有一个副作用方法(没有返回),我会检查该方法是否被调用,我可能应该在存根类中添加一个布尔值或计数器来计算该方法被调用的次数。


结论

存根通常需要为您的单元测试编写大量开销/代码。由于提供了开箱即用的记录/验证功能,模拟阻止了什么。
这就是为什么现在,随着优秀模拟库的出现,存根方法在实践中很少使用。


关于Martin Fowler的文章:当我使用模拟并且避免存根时,我不认为自己是一个“嘲讽者”程序员。
但是我在真正需要的时候使用mock(恼人的依赖项),当我测试一个具有依赖项的类时,我喜欢测试切片和迷你集成测试,这将是一个开销。

我读了单元测试的艺术,偶然发现了以下定义:

假的是一个通用术语,可用于描述存根或模拟对象(手写或其他方式),因为它们看起来都像真实对象。假存根或模拟取决于它在当前测试中的使用方式。如果它用于检查交互(断言),它是模拟对象。否则,它是存根

加上有用的答案,最使用Mocks而不是Subs的强大点之一

如果协作者[主代码依赖它]不在我们的控制之下(例如来自第三方库),
在这种情况下,存根比模拟更难写

存根是将值返回给SUT的测试双精度对象。

模拟是一个测试二重奏,测试使用它来验证SUT是否正确调用了依赖项。

而且mock通常是个存根

存根

存根是一个用于伪造具有预编程行为的方法的对象。您可能希望使用它而不是现有方法,以避免不必要的副作用(例如,存根可能会进行虚假的获取调用,该调用返回预编程的响应,而无需实际向服务器发出请求)。

Mock

模拟是一个对象,用于伪造一个具有预编程行为预先设定的期望的方法。如果这些期望没有得到满足,那么模拟将导致测试失败(例如,模拟可以发出一个虚假的获取调用,返回预编程的响应,而不实际向服务器发出期望的请求,例如第一个参数是"http://localhost:3008/",否则测试将失败。)

差异

与模拟不同,存根没有可能无法通过测试的预编程期望。

模拟:有助于模拟和检查即将到来的交互是SUT对其依赖项进行的调用以更改其状态。

存根:帮助模拟传入的交互。这些交互是调用SUT利用它的依赖关系来获取输入数据。

在此处输入图片描述

来源:单元测试原则、实践和模式-Manning

存根

存根是保存预定义数据并使用它在测试期间接听调用的对象。当您不能或不想涉及将使用真实数据接听或具有不良副作用的对象时,可以使用它。

一个例子可以是一个需要从数据库中获取一些数据来响应方法调用的对象。我们引入了一个存根并定义了应该返回什么数据,而不是真正的对象。

输入图片描述


存根示例:

public class GradesService {
private final Gradebook gradebook;
public GradesService(Gradebook gradebook) {this.gradebook = gradebook;}
Double averageGrades(Student student) {return average(gradebook.gradesFor(student));}}

您无需从Gradebook存储调用数据库来获取真实学生的成绩,而是预先配置将返回的成绩存根。您定义了足够的数据来测试平均计算算法。

public class GradesServiceTest {
private Student student;private Gradebook gradebook;
@Beforepublic void setUp() throws Exception {gradebook = mock(Gradebook.class);student = new Student();}
@Testpublic void calculates_grades_average_for_student() {//stubbing gradebookwhen(gradebook.gradesFor(student)).thenReturn(grades(8, 6, 10));
double averageGrades = new GradesService(gradebook).averageGrades(student);
assertThat(averageGrades).isEqualTo(8.0);}}


Mock

模拟是注册它们接收到的调用的对象。在测试断言中,您可以在Mocks上验证是否执行了所有预期的操作。当您不想调用生产代码或没有简单的方法验证该预期代码已执行时,您可以使用模拟。没有返回值,也没有检查系统状态变化的简单方法。一个例子可以是调用电子邮件发送服务的功能。

您不希望每次运行测试时都发送电子邮件。此外,在测试中验证是否发送了正确的电子邮件并不容易。您唯一能做的就是验证在我们的测试中使用的功能的输出。在其他世界中,验证是否调用了电子邮件发送服务。

输入图片描述


模拟示例:

public class SecurityCentral {
private final Window window;private final Door door;
public SecurityCentral(Window window, Door door) {this.window = window;this.door = door;}
void securityOn() {window.close();door.close();}}

您不想关闭真正的门来测试安全方法是否有效,对吧?相反,您在测试代码中放置门和窗模拟对象。

public class SecurityCentralTest {
Window windowMock = mock(Window.class);Door doorMock = mock(Door.class);
@Testpublic void enabling_security_locks_windows_and_doors() {SecurityCentral securityCentral = new SecurityCentral(windowMock, doorMock);
securityCentral.securityOn();
verify(doorMock).close();verify(windowMock).close();}}

非常感谢MichaüLipski的好文章。进一步阅读:

双倍测试-Martin Fowlerhttps://martinfowler.com/bliki/TestDouble.html
Test Double-xUnit模式http://xunitpatterns.com/Test%20Double.html
嘲笑不是存根-Martin Fowlerhttps://martinfowler.com/articles/mocksArentStubs.html
命令查询分离-Martin Fowlerhttps://martinfowler.com/bliki/CommandQuerySeparation.html

有很多很棒的答案,我喜欢这一个,所以我把它变成了一张桌子。

API
虚拟存根模拟
API
状态X
XX
行为XXX