模拟和存根有什么区别?

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


#1楼

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


#2楼

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

存根

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

嘲笑

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

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


#3楼

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

让我尝试在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


#4楼

前言

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

参考

根据马丁·福勒的文章

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

样式

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

原理

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

生命周期

使用存根测试生命周期:

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

使用模拟测试生命周期:

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

摘要

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

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


#5楼

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

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

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


#6楼

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

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


#7楼

请参见下面使用C#和Moq框架的模拟与存根示例。 Moq没有用于存根的特殊关键字,但是您也可以使用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()
        {
            // 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";
        }
    }
}

#8楼

伪造品是一个通用术语,可用来描述存根或模拟对象(手写或其他形式),因为它们看起来都像真实对象。

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

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

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

 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类的结果。


#9楼

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

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

#10楼

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

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

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

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

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

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

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

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

    示例:您正在测试用户注册类。 调用Save ,应调用SendConfirmationEmail

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


#11楼

我在答案中使用了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示例编写类似的书,那将是很棒的。 如果有人知道这本书,请分享。 干杯:)


#12楼

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

单元测试->存根

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

单元测试根据单元的结果和状态进行断言

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

一个Mock 就像一个存根,只有它也有一些方法可以确定在Mock上调用了哪些方法 。 因此,可以使用模拟程序测试单元是否可以正确处理各种返回值,以及单元是否正确使用了协作者。 例如,您无法通过dao对象返回的值来查看是否使用Statement或PreparedStatement从数据库中读取了数据。 您也看不到在返回值之前是否调用了connection.close()方法。 模拟可以做到这一点。 换句话说,模拟使测试单位与协作者的完整交互成为可能。 不仅返回单位使用的值的协作方法。 在单元测试中使用模拟可以表示为:

单元测试->模拟

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

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

单元测试对模拟调用的方法进行断言

详细信息>> 这里


#13楼

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

function foo(){}

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

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

在断言和状态方面:

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

参考文献


#14楼

要非常清楚和实用:

存根(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!');
   }
}

#15楼

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

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

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

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

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


#16楼

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

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


#17楼

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


#18楼

以下是我的理解...

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

  • 当您从真实服务提供商处获取测试数据时,尽管是从接口的测试版本中获取对象的测试版本,但您正在使用存根,存根可以具有接受某些输入并提供相应输出以帮助您执行的逻辑状态验证...


#19楼

存根

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

嘲笑

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

存根和存根之间的区别

用模拟编写的测试通常遵循initialize -> set expectations -> exercise -> verify测试模式。 虽然预写的存根将遵循initialize -> exercise -> verify

存根和存根之间的相似性

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


#20楼

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

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

因此,主要区别是:

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

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


#21楼

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

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


#22楼

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


#23楼

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


#24楼

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

在此处输入图片说明

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


#25楼

存根和模拟测试的观点:

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

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


#26楼

那里有很多有效的答案,但我认为值得一提的是这种形式的鲍伯叔叔: https : //8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html

有史以来最好的解释!


#27楼

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

#28楼

让我们来看看测试双打:

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

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


#29楼

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

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

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

假人将是他甚至没有使用过的叉子……而间谍可能会像提供您已经使用过的解释一样。


#30楼

Mockito的例子

在此输入图像描述

Stub只返回固定数据。 存根很简单直接 - 它们本质上是方法的最简单实现,并且每次都返回相同的固定数据。 这使我们可以完全控制从依赖项上调用的方法返回的值。

Mock对象提供了一种检查被测对象是否已调用某些方法的方法。

正如Martin Fowler在他的文章中所说的那样

存在区别在于stub使用状态验证而mock使用行为验证。

在这里这里阅读更多


#31楼

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值