模拟和存根有什么区别?


961

我已经阅读了有关测试中的模拟与存根的各种文章,包括Martin Fowler的Mocks Are n't Stubs,但仍然不了解它们之间的区别。



75
@OP因为没有区别。受到社区的热爱,这篇文章-受到所有应有的尊重-通过在易于理解的单词上添加其他含义并使不必要的事情变得复杂,使所有不必要的事情变得混乱。模拟只是一种模拟,它运行的是虚假的业务逻辑,而不是真实的逻辑。最终检查行为是您的选择,但这仍然是一个模拟。或任何您想调用的名称,但将其设为1。不要分裂头发。保持简单,这样人们就可以轻松理解您的概念-上面的文章确实失败了。
2016年

10
“在整个文献中,模拟,假冒和存根之间的分类是非常不一致的。” 有很多引用。仍然是我最喜欢的Wikipedia引文之一-如果存在这样的话:) en.wikipedia.org/wiki/Mock_object
JD。

11
Martin Fowler的文章对于初学者来说真的很难理解。
lmiguelvargasf

1
我的理解是,存根只是测试的一个废弃对象,就像一组虚假数据一样。模拟将是更复杂的东西的巧妙覆盖版本,例如带有各种方法的服务层,您可能已经更改了它们的行为以进行测试。这两件事一起使用,就像您可以将一些存根对象传递到模拟图层中一样。
JsonStatham

Answers:


745

存根

我相信最大的区别是您已经以预定的行为编写了存根。因此,您将拥有一个类,该类实现您为测试目的而伪装的依赖项(最有可能是抽象类或接口),并且该方法将仅通过设置的响应进行处理。他们不会做任何花哨的事情,并且您已经在测试之外为其编写了存根代码。

嘲笑

模拟是在测试过程中必须设置的期望值。模拟不是以预定的方式设置的,因此您具有在测试中执行该模拟的代码。嘲笑是在运行时确定的,因为设置期望的代码必须在它们执行任何操作之前运行。

存根和存根之间的区别

用模拟编写的测试通常遵循一种initialize -> set expectations -> exercise -> verify测试模式。而预写的存根将跟随一个initialize -> exercise -> verify

存根和存根之间的相似性

两者的目的都是消除测试一个类或函数的所有依赖关系,以便您的测试在尝试证明时更加专注和简单。


875

前言

对象有几种定义,它们不是真实的。通用词是test double。该术语包括:虚拟伪造存根模拟

参考

根据马丁·福勒的文章

  • 虚拟对象会传递,但从未实际使用过。通常它们仅用于填充参数列表。
  • 对象实际上具有有效的实现,但是通常采取一些捷径,这使其不适合生产(内存数据库是一个很好的示例)。
  • 存根提供对测试过程中进行的呼叫的固定答复,通常不响应为测试编程的内容之外的任何内容。存根还可以记录有关呼叫的信息,例如电子邮件网关存根,它可以记住“已发送”的消息,或者仅记住“已发送”的消息数量。
  • 嘲笑是我们在这里谈论的内容:对象被预先编程并带有期望,形成了期望接收的呼叫的规范。

样式

嘲弄与存根=行为测试与状态测试

原理

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

生命周期

使用存根测试生命周期:

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

使用模拟测试生命周期:

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

摘要

模拟和存根测试都为以下问题提供了答案:结果如何?

使用模拟进行测试也感兴趣:如何获得结果?


等等,模拟还返回固定答案吗?否则,为什么他们要回答这个问题?
AturSams

从您写的内容中可以看出,模拟=存根+期望与验证,因为模拟“为测试过程中的调用提供了固定的答案,通常根本不响应测试所编程的内容”(与存根相同)。Fowler作为存根示例显示的示例实际上是间谍的示例!这意味着模拟是存根,而间谍是存根。存根只是具有多种工作方法的对象。这也解释了为什么Mockito不推荐使用stub()方法。
kolobok

我对此感到困惑,公认的答案是这种“期望设置”,这甚至意味着什么?通常,在“主代码”中创建所需的结果。听起来您好像以某种方式将期望放入模拟对象中,这对我来说没有意义。此外,您可以轻松地通过一些输入进行模拟,存储结果,稍后创建“期望”,然后进行比较。您使用的术语我太抽象和模棱两可。
IceFire

364

存根是一个简单的伪造对象。它只是确保测试顺利进行。
模拟是更聪明的存根。您验证您的测试通过了。


33
我认为这是最简洁明了的答案。外卖:模拟IS-A存根。 stackoverflow.com/a/17810004/2288628是此答案的较长版本。
PoweredByRice 2014年

8
我认为模拟不是存根。嘲笑用于断言,并且永不返回数据,存根用于返回数据,并且永不断言。
dave1010 2015年

2
@ dave1010 Mocks最肯定可以返回数据甚至引发异常。他们应该这样做以响应传递给它们的参数。
特伦顿

2
@trenton如果对象根据传入的数据返回或抛出,则它是假的,而不是模拟的。存根测试您的SUT如何处理接收消息,模拟测试您的SUT如何发送消息。混合使用2可能会导致不良的OO设计。
dave1010

8
我认为这很棒-存根返回问题的答案。模拟还返回问题的答案(是存根),但也可以验证问题是否存在!!
Leif

238

这是每个示例的说明,后面是真实示例。

  • 虚拟 -只是虚假的价值观来满足API

    示例:如果要测试的类的方法在构造函数中需要许多强制性参数,而这些参数对测试没有影响,则可以创建虚拟对象以创建类的新实例。

  • 伪造 -创建一个类的测试实现,该类可能依赖于某些外部基础结构。(优良作法是您的单元测试实际上与外部基础结构交互。)

    示例:创建用于访问数据库的伪造实现,将其替换为in-memorycollection。

  • 存根重写方法可返回硬编码的值,也称为state-based

    示例:您的测试课程取决于Calculate()需要5分钟才能完成的方法。无需等待5分钟,您可以用返回硬编码值的存根替换其实际实现。仅花费一小部分时间。

  • 模拟 -非常类似于Stubinteraction-based不是基于状态的。这意味着您不希望from Mock从中返回任何值,而是假设已完成特定的方法调用顺序。

    示例:您正在测试用户注册类。打电话后Save,应该打电话SendConfirmationEmail

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


175

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

存根

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

嘲笑

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

因此,正如肖恩·哥本哈根(Sean Copenhaver)在回答中所描述的那样,区别在于嘲笑设定了期望(即,对它们是否被调用或如何被调用进行断言)。


为了补充Dillon的帖子,请考虑一下,您有一个名为“ MakeACake”的类,该类具有几个库:牛奶,鸡蛋,糖,烤箱。
aarkerio 2014年

139

存根不会使您的测试失败,模拟可以。


2
我认为这很好,您知道重构后测试是否具有相同的行为。
RodriKing

1
@RodriKing我也有同样的感觉。与Mock一样,生产代码中有任何更改-您对测试代码也有相应的更改。哪个好痛!使用存根,感觉就像您一直在测试行为,因此不需要对测试代码进行微更改。
tucq88 '19

35

我认为Roy Osherove在他的书《单元测试的艺术》(第85页)中给出了关于这个问题的最简单,更清晰的答案。

告诉我们正在处理存根的最简单方法是注意到存根永远不会通过测试。测试使用的断言始终与被测类相对。

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

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

这意味着,如果您对假货进行断言,则意味着您将假货用作模拟,如果仅使用假货来运行测试而没有断言,则将假货用作存根。


2
希望您的回答能找到最好的答案。这是R. Osherove解释的youtu.be/fAb_OnooCsQ?t=1006
Michael Ekoka,

31

阅读以上所有说明,让我尝试凝结一下:

  • 存根(Stub):一段伪代码,可以运行测试,但是您并不关心会发生什么。
  • 模拟:一段伪代码,您通过验证将正确调用VERIFY作为测试的一部分。
  • 间谍:一段伪代码,它拦截对真实代码的某些调用,使您可以在不替换整个原始对象的情况下验证调用。

4
好答案。根据您的定义,模拟声音听起来与间谍非常相似。如果您将答案更新为包括更多的测试双打,那就太好了。
罗恩·

我写这个答案的时候没有听说过间谍。
奥鲁尼

23

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

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


19
“你是什么意思,苹果的方式?” 使用Helvetica
kubi 2010年

7
以Apple方式而非以Microsoft方式:)
never_had_a_name 2010年

2
这对情况有帮助吗?
NebulaFox 2010年

21

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

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

模拟就像实际上进入了该方法,并确保返回的正确值之前,确保内部所有内容正确。


20

使用心理模型确实可以帮助我理解这一点,而不是所有的解释和文章都不太“深入”。

想象您的孩子在桌子上有一块玻璃板,他开始玩耍。现在,您担心它会破裂。因此,您改为给他一个塑料盘子。那将是一个模拟(相同的行为,相同的界面,“更软”的实现)。

现在,说您没有塑料替代品,因此请解释“如果继续使用它,它将损坏!”。那是一个存根,您预先提供了预定义的状态。

一个假人将他甚至没有用叉子...和间谍可能是像你提供已经使用该工作了相同的解释。


19

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

让我尝试在WHY存根WHY模拟中进行解释

假设我正在为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方法,而且我不确定需要为ui对象编写哪些方法。我希望通过编写测试代码来了解我的控制器将如何与ui对象协作。

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

如果您想进一步了解模拟,建议阅读这篇文章:http : //jmock.org/oopsla2004.pdf


1
我认为您的想法正确,但是Dillon Kearns对此进行了更清晰的解释。
O'Rooney 2014年

19

要非常清楚和实用:

存根(Stub):一个类或对象,用于实现要伪造的类/对象的方法,并始终返回所需的内容。

JavaScript中的示例:

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

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

正如@mLevan所说,以您正在测试用户注册类为例。调用保存后,应调用SendConfirmationEmail。

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

var Mock = {
   calls: {
      method_a: 0
   }

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

16

这张幻灯片很好地解释了主要区别。

在此处输入图片说明

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


这是对IMO之间的区别的更清楚的解释。对于存根:测试人员将存根并直接在被测类中使用。但是对于Mock,测试人员必须以设备方式使用Mock对象。在不同的情况下,它的行为会有所不同。相比之下,存根的行为并不会有所不同,而是按原样使用(意味着在每次联系时都返回相同的数据)
Dexter

12

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

创建的每个类或对象都是伪造的。如果您验证对它的呼叫,则它是一个模拟。否则,它是一个存根。


12
  • 存根与模拟
    • 存根
      1. 提供方法调用的特定答案
        • 例如:myStubbedService.getValues()仅返回被测代码所需的字符串
      2. 被测试的代码用来隔离它
      3. 不能通过测试
        • 例如:myStubbedService.getValues()仅返回存根值
      4. 经常实现抽象方法
    • cks
      1. 存根的“超集”;可以断言某些方法被调用
        • 例如:验证myMockedService.getValues()仅被调用一次
      2. 用于测试被测代码的行为
      3. 可能无法通过测试
        • 例如:验证myMockedService.getValues()被调用一次;验证失败,因为我的测试代码未调用myMockedService.getValues()
      4. 经常嘲笑接口

11

让我们来看看测试双打:

  • 伪造品伪造品具有有效的实现,但与生产实现不同。:在内存中实现数据访问对象或存储库。
  • 存根(Stub):存根是一个对象,用于保存预定义的数据,并在测试期间将其用于应答呼叫。:需要从数据库中获取一些数据以响应方法调用的对象。

  • 嘲笑:嘲笑是注册收到的呼叫的对象。在测试断言中,我们可以在Mocks上验证是否已执行所有预期的操作。:调用电子邮件发送服务的功能。要了解更多,只需检查一下


1
我认为最好的答案
Ero Stefano

9

一个假的是可以用来描述任何存根或模拟对象(手写或其他方式)的总称,因为他们看起来像真正的对象。

假货是存根还是假货,取决于当前测试中的使用方式。如果用于检查交互(认定为无效),则它是一个模拟对象。否则,它是一个存根。

伪造品可确保测试顺利进行。这意味着您将来的测试的读者将了解假对象的行为,而无需读取其源代码(而无需依赖外部资源)。

测试顺利进行意味着什么?
例如下面的代码:

 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");
                }
            }
        }

您要测试mailService.SendEMail()方法,这样做需要在测试方法中模拟一个Exception,因此您只需要创建一个Fake Stub errorService类来模拟该结果,然后您的测试代码就可以测试mailService.SendEMail()方法。如您所见,您需要模拟另一个外部Dependency ErrorService类的结果。


8

由jMock的开发人员在论文《模拟角色而不是对象》中提出:

存根是返回固定结果的生产代码的虚拟实现。模拟对象充当存根,但还包括断言以检测目标对象与其邻居的交互。

因此,主要区别是:

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

总而言之,同时还试图消除Fowler文章标题中的困惑:模拟是存根,但它们不仅是存根


1
我认为您是对的,但这就是Fowler文章令人困惑的原因,文章标题为“ Mocks Are n't Stubs” ...但它们是吗?_(ツ)_ /¯
stonedauwg

的确,@ stonedauwg,我编辑了我的帖子,以结合您的双关语和说明。希望这会有所帮助。
Dimos

@stonedauwg,模拟不是存根,就像矩形不是正方形一样。:)
seanriordan08年

7

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

一个是可以用来描述任何存根或模拟对象(手写或其他方式)的总称,因为他们看起来像真正的对象。假货是存根还是假货,取决于当前测试中的使用方式。如果用于检查交互作用(认定为无效),则为模拟对象。否则,它是一个存根


5

我偶然发现了UncleBob的《小小嘲笑者》这个有趣的文章。它以一种非常容易理解的方式解释了所有术语,因此对初学者很有用。马丁·福尔斯(Martin Fowlers)的文章特别是对于像我这样的初学者来说,是一本精读的文章。


4

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

模拟是运行测试的伪造对象。我们把断言放在哪里。


“因此,我们不是直接与数据库交互,而是与Hashmap交互。” ...因为还没有时间编写数据库模块的代码,因此如果不使用存根,我们将无法运行测试代码。否则,完全相同的Hasmap将是一个模拟!对?
鲍里斯·达彭(BorisDäppen),2015年

4

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

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()
        {
            // Arrange 
            var mockEntityRepository = new Mock<IEntityRepository>();
            mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));

            var entity = new EntityClass(mockEntityRepository.Object);
            // Act 
            var name = entity.GetNameWithPrefix(12);
            // Assert
            mockEntityRepository.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()
        {
            // Arrange 
            var mockEntityRepository = new Mock<IEntityRepository>();
            mockEntityRepository.Setup(m => m.GetName(It.IsAny<int>()));
            var entity = new EntityClass(mockEntityRepository.Object);
            // Act 
            var name = entity.GetNameWithPrefix(0);
            // Assert
            mockEntityRepository.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()
        {
            // Arrange 
            var 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);
            // Act 
            var name = entity.GetNameWithPrefix(12);
            // Assert
            Assert.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 Id
            return "NameFromDb";
        }
    }
}

4

存根和模拟测试的观点:

  • Stub是由用户以静态方式完成的虚拟实现,即在Stub中编写实现代码。因此它无法处理服务定义和动态条件,通常这是在JUnit框架中完成的,而不使用模拟框架。

  • Mock也是虚拟实现,但其实现通过使用Mockito等Mocking框架以动态方式完成。因此,我们可以以动态方式处理条件和服务定义,即可以在运行时从代码动态创建模拟。因此,使用模拟我们可以动态实现存根。


3

加上有用的答案,使用Mocks比Subs强大的功能之一

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


2

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

Stub -Stubbing是一种软件开发技术,用于在开发生命周期的早期实现类的方法。它们通常用作占位符,用于实现已知接口,在该接口中接口已完成或已知,但实现尚不知道或尚未完成。您从存根开始,这仅意味着您仅写下函数的定义,并保留实际代码以备后用。好处是您不会忘记方法,并且可以在代码中看到它的同时继续考虑您的设计。您还可以让存根返回静态响应,以便该响应可以立即被代码的其他部分使用。存根对象提供了有效的响应,但是无论您传入什么输入,它都是静态的,您将始终获得相同的响应:

class Foo(object):
    def bar1(self):
        pass

    def bar2(self):
        #or ...
        raise NotImplementedError

    def bar3(self):
        #or return dummy data
        return "Dummy Data"

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

mymodule.py:

import os
import os.path

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

test.py:

from mymodule import rm
import mock
import 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 parameters
        mock_os.remove.assert_called_with("any path")

if __name__ == '__main__':
    unittest.main()

这是一个非常基本的示例,它仅运行rm并声明调用它的参数。您不仅可以将模拟与对象一起使用,还可以返回一个值,以便可以使用模拟对象代替存根进行测试。

有关unittest.mock的更多信息,python 2.x模拟中的注释未包含在unittest中,而是一个可下载的模块,可以通过pip(pip安装模拟)进行下载。

我还阅读了Roy Osherove的“单元测试的艺术”,我认为如果使用Python和Python示例编写类似的书,那将是很棒的。如果有人知道这本书,请分享。干杯:)


2

存根是为测试目的而构建的伪造对象。模拟是一个存根,它记录是否有效地发生了预期的呼叫。


2

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

function foo(){}

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

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

在断言和状态方面:

  • 在事件或状态更改之前会断言
  • 存根未声明,它们在事件发生前提供状态,以避免从无关单元执行代码
  • 像桩一样设置间谍,然后在事件或状态更改后声明它们
  • 伪造没有被断言,它们在具有硬编码依赖项的事件之后运行以避免状态

参考文献


2
+1用于向词汇表添加间谍。另外,我认为您的意思是“间谍像
模拟程序


2

模拟既是技术性的又是功能性的对象对象。

模拟是技术性的。它的确是由一个模拟库(EasyMock,JMockit和最近的Mockito都以此为原型)创建的,这要归功于字节码的生成
模拟实现以一种我们可以检测的方式生成使在执行方法调用时其返回特定值,还可以进行其他操作,例如验证是否使用某些特定参数(严格检查)或任何参数(没有严格的检查)。

实例化一个模拟:

@Mock Foo fooMock

记录行为:

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

验证调用:

verify(fooMock).hello()

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

但是该模拟功能也是功能性的,因为它是我们需要与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的文章:当我使用模拟并且避免存根时,我不认为自己是“模拟主义者”程序员。
但是我在真正需要时使用了模拟(使依赖项烦恼),并且当我测试具有依赖项的类时,我更喜欢测试切片和小型集成测试,而模拟将是一项开销。

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.