我已经阅读了各种关于测试中模拟与存根的文章,包括 Martin Fowler's Mocks Aren't Stubs,但仍然不明白其中的区别。
前言
对象有几种定义,它们是不真实的。通用术语是测试替身。该术语包括:dummy、fake、stub、mock。
参考
虚拟对象被传递但从未实际使用过。通常它们仅用于填充参数列表。假对象实际上有工作实现,但通常采取一些捷径,这使得它们不适合生产(内存数据库就是一个很好的例子)。存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。存根还可以记录有关呼叫的信息,例如记住它“发送”的消息的电子邮件网关存根,或者可能只记录它“发送”的消息的数量。模拟就是我们在这里讨论的内容:预先编程的对象具有期望,这些期望形成了它们期望接收的调用的规范。
风格
模拟 vs 存根 = 行为测试 vs 状态测试
原则
根据Test only one thing per test的原则,一个测试可能有多个stub,但一般只有一个mock。
生命周期
使用存根测试生命周期:
设置 - 准备正在测试的对象及其存根协作者。练习 - 测试功能。验证状态 - 使用断言检查对象的状态。 Teardown - 清理资源。
使用模拟测试生命周期:
设置数据 - 准备正在测试的对象。设置期望 - 在主要对象使用的模拟中准备期望。练习 - 测试功能。验证期望 - 验证是否在模拟中调用了正确的方法。验证状态 - 使用断言检查对象的状态。 Teardown - 清理资源。
概括
模拟和存根测试都给出了这个问题的答案:结果是什么?
使用 mocks 进行测试也很感兴趣:结果是如何实现的?
存根
我相信最大的区别是您已经编写了具有预定行为的存根。因此,您将拥有一个实现您为测试目的而伪造的依赖项(最有可能是抽象类或接口)的类,并且这些方法将被设置响应。他们不会做任何花哨的事情,而且您已经在测试之外为其编写了存根代码。
嘲笑
模拟是作为测试的一部分,您必须根据自己的期望进行设置。模拟不是以预先确定的方式设置的,因此您有在测试中执行它的代码。在某种程度上,模拟是在运行时确定的,因为设置期望的代码必须在它们做任何事情之前运行。
模拟和存根之间的区别
使用 mock 编写的测试通常遵循 initialize -> set expectations -> exercise -> verify
模式进行测试。而预先编写的存根将遵循 initialize -> exercise -> verify
。
模拟和存根之间的相似之处
两者的目的都是为了消除对类或函数的所有依赖项的测试,以便您的测试在他们试图证明的内容上更加集中和简单。
A mock is something that as part of your test you have to setup with your expectations. A mock is not setup in a predetermined way so you have code that does it in your test.
那么,它是根据您的期望设置的,但不是以预先确定的方式?这怎么可能?
存根是一个简单的假对象。它只是确保测试顺利运行。模拟是更智能的存根。您验证您的测试通过它。
这是对每一个的描述,然后是真实世界的样本。
Dummy - 只是为了满足 API 的虚假值。示例:如果您正在测试一个类的方法,该方法在构造函数中需要许多强制参数,而这对您的测试没有影响,那么您可以创建虚拟对象来创建类的新实例。
Fake - 创建一个可能依赖于某些外部基础设施的类的测试实现。 (最好的做法是您的单元测试实际上并不与外部基础设施交互。)示例:创建用于访问数据库的假实现,将其替换为内存中的集合。
存根 - 覆盖方法以返回硬编码值,也称为基于状态。示例:您的测试类依赖于Calculate() 方法,需要5 分钟才能完成。您可以用返回硬编码值的存根替换它的实际实现,而不是等待 5 分钟;只占用一小部分时间。
Mock - 与 Stub 非常相似,但基于交互而不是基于状态。这意味着您不期望 Mock 返回一些值,而是假设进行了特定的方法调用顺序。示例:您正在测试用户注册类。调用 Save 后,它应该调用 SendConfirmationEmail。
Stubs
和 Mocks
实际上是 Mock
的子类型,它们都将实际实现与测试实现互换,但出于不同的具体原因。
在 codeschool.com 课程 Rails Testing for Zombies 中,他们给出了以下术语定义:
存根
用于用返回指定结果的代码替换方法。
嘲笑
带有方法被调用的断言的存根。
因此,正如 Sean Copenhaver 在他的回答中所描述的那样,不同之处在于模拟设置了期望(即断言,关于它们是否或如何被调用)。
存根不会使您的测试失败,模拟可以。
看了上面所有的解释,让我试着浓缩一下:
存根:一段让测试运行的虚拟代码,但你不关心它会发生什么。替代实际工作代码。
模拟:您验证的一段虚拟代码作为测试的一部分被正确调用。替代实际工作代码。
Spy:一段虚拟代码,拦截并验证对真实工作代码的一些调用,避免替换所有真实代码的需要。
我认为 Roy Osherove 在他的书 The art of Unit Testing (page 85) 中给出了关于这个问题的最简单和更清晰的答案
判断我们正在处理存根的最简单方法是注意存根永远不会通过测试。测试使用的断言总是针对被测类。另一方面,测试将使用模拟对象来验证测试是否失败。 [...] 同样,模拟对象是我们用来查看测试是否失败的对象。
存根和模拟都是假的。
如果您对假货进行断言,则意味着您将假货用作模拟,如果您仅使用假货来运行测试而不对其进行断言,则您将假货用作存根。
Mock 只是测试行为,确保调用某些方法。存根是特定对象的可测试版本(本身)。
苹果的方式是什么意思?
如果将其与调试进行比较:
Stub 就像确保方法返回正确的值 Mock 就像实际进入方法并确保在返回正确的值之前内部的所有内容都是正确的。
要非常清楚和实用:
存根:实现要伪造的类/对象的方法并始终返回您想要的内容的类或对象。
JavaScript 中的示例:
var Stub = {
method_a: function(param_a, param_b){
return 'This is an static result';
}
}
模拟:与存根相同,但它添加了一些逻辑来“验证”何时调用方法,因此您可以确定某些实现正在调用该方法。
正如@mLevan 所说,想象一下您正在测试用户注册类。调用 Save 后,它应该调用 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!');
}
}
让我们看看测试双打:
Fake:Fake 是具有工作实现的对象,但与生产对象不同。如:数据访问对象或存储库的内存实现。
存根:存根是一个保存预定义数据并在测试期间使用它来应答呼叫的对象。如:需要从数据库中抓取一些数据来响应方法调用的对象。
模拟:模拟是注册他们收到的调用的对象。在测试断言中,我们可以在 Mocks 上验证所有预期的操作都已执行。如:调用电子邮件发送服务的功能。有关更多信息,请查看此内容。
这张幻灯片很好地解释了主要区别。
https://i.stack.imgur.com/frwt1.png
*摘自华盛顿大学 CSE 403 第 16 课(由“Marty Stepp”制作的幻灯片)
使用心智模型确实帮助我理解了这一点,而不是所有的解释和文章,这些解释和文章并没有完全“深入”。
想象一下,您的孩子在桌子上有一个玻璃盘子,他开始玩它。现在,您担心它会破裂。所以,你给他一个塑料盘子。那将是一个 Mock (相同的行为,相同的接口,“更软”的实现)。
现在,假设你没有塑料替代品,所以你解释说“如果你继续玩它,它会坏掉的!”。那是一个存根,你预先提供了一个预定义的状态。
一个 Dummy 将是他甚至没有使用的叉子......而 Spy 可能就像提供您已经使用过的相同解释一样有效。
我认为他们之间最重要的区别是他们的意图。
让我尝试在 WHY stub vs. WHY mock 中解释它
假设我正在为我的 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 对象协作。
通过编写mock,您通过验证期望是否满足来发现对象的协作关系,而stub只是模拟对象的行为。
如果您想了解更多关于模拟的信息,我建议您阅读这篇文章:http://jmock.org/oopsla2004.pdf
存根与模拟 存根为方法调用提供特定答案 例如:myStubbedService.getValues() 只返回被测代码所需的字符串,被测代码用来隔离它不能失败测试例如:myStubbedService.getValues() 只返回存根value 经常实现抽象方法模拟存根的“超集”;可以断言某些方法被调用 例如:验证 myMockedService.getValues() 仅被调用一次用于测试被测代码的行为 测试失败 例如:验证 myMockedService.getValues() 是否被调用一次;验证失败,因为我的测试代码没有调用 myMockedService.getValues() 经常模拟接口
存根为方法调用提供特定的答案 例如:myStubbedService.getValues() 只返回一个被测试代码所需的字符串,被测试代码用来隔离它不能失败测试例如:myStubbedService.getValues() 只返回存根值通常实现抽象方法
为方法调用提供具体答案 例如:myStubbedService.getValues() 只返回被测代码所需的字符串
例如: myStubbedService.getValues() 只返回被测代码所需的字符串
被测试代码用来隔离它
不能失败测试前: myStubbedService.getValues() 只返回存根值
例如: myStubbedService.getValues() 只返回存根值
经常实现抽象方法
模拟存根的“超集”;可以断言某些方法被调用 例如:验证 myMockedService.getValues() 仅被调用一次用于测试被测代码的行为 测试失败 例如:验证 myMockedService.getValues() 是否被调用一次;验证失败,因为我的测试代码没有调用 myMockedService.getValues() 经常模拟接口
存根的“超集”;可以断言某些方法被调用 ex:验证 myMockedService.getValues() 只被调用一次
例如:验证 myMockedService.getValues() 是否只被调用一次
用于测试被测代码的行为
测试失败:验证 myMockedService.getValues() 是否被调用过一次;验证失败,因为我的测试代码没有调用 myMockedService.getValues()
例如:验证 myMockedService.getValues() 是否被调用过一次;验证失败,因为我的测试代码没有调用 myMockedService.getValues()
经常模拟接口
我正在阅读 The Art of Unit Testing,偶然发现了以下定义:
fake 是一个通用术语,可用于描述存根或模拟对象(手写或其他),因为它们看起来都像真实对象。 fake 是 stub 还是 mock 取决于它在当前测试中的使用方式。如果它用于检查交互(断言反对),它是一个模拟对象。否则,它是一个存根。
存根
存根是用于伪造具有预编程行为的方法的对象。您可能希望使用此方法而不是现有方法以避免不必要的副作用(例如,存根可以进行虚假的 fetch 调用,该调用返回预编程的响应而无需实际向服务器发出请求)。
嘲笑
mock 是一个对象,用于伪造具有预编程行为以及预编程期望的方法。如果这些期望没有得到满足,那么模拟将导致测试失败(例如,模拟可能会进行虚假的 fetch 调用,该调用会返回预编程的响应,而不会实际向服务器发出期望的请求例如,第一个参数是 "http://localhost:3008/"
否则测试将失败。)
区别
与模拟不同,存根没有预先编程的期望,可能会使您的测试失败。
他使用的通用术语是测试替身(想想特技替身)。 Test Double 是一个通用术语,用于替换生产对象以进行测试的任何情况。 Gerard 列出了多种类型的 double:
虚拟对象被传递但从未实际使用过。通常它们仅用于填充参数列表。
假对象实际上有工作实现,但通常采取一些捷径,这使得它们不适合生产(InMemoryTestDatabase 就是一个很好的例子)。
存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。
间谍是存根,它还根据调用方式记录一些信息。其中一种形式可能是电子邮件服务,它记录发送了多少条消息(也称为部分模拟)。
模拟预编程了期望,这些期望形成了他们期望接收的调用的规范。如果他们收到了他们不期望的呼叫,他们可以抛出异常,并在验证过程中进行检查以确保他们得到了他们期望的所有呼叫。
fake 是一个通用术语,可用于描述存根或模拟对象(手写或其他),因为它们看起来都像真实对象。
fake 是 stub 还是 mock 取决于它在当前测试中的使用方式。如果它用于检查交互(断言反对),它是一个模拟对象。否则,它是一个存根。
假货确保测试顺利进行。这意味着您未来测试的读者将了解假对象的行为,而无需阅读其源代码(无需依赖外部资源)。
测试运行顺利是什么意思?例如在下面的代码中:
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() 方法,为此您需要在测试方法中模拟一个异常,因此您只需要创建一个 Fake Stub errorService 类来模拟该结果,然后您的测试代码就可以测试mailService.SendEMail() 方法。如您所见,您需要模拟来自另一个 External Dependency ErrorService 类的结果。
来自 jMock 开发人员的论文 Mock Roles, not Objects:
存根是返回预设结果的生产代码的虚拟实现。模拟对象充当存根,但也包括断言以检测目标对象与其邻居的交互。
因此,主要区别在于:
在存根上设置的期望通常是通用的,而在模拟上设置的期望可能更“聪明”(例如在第一次调用时返回 this,在第二次调用时返回 this 等)。
存根主要用于设置 SUT 的间接输入,而模拟可用于测试 SUT 的间接输入和间接输出。
总而言之,同时还试图消除来自 Fowler's article 标题的混淆:模拟是存根,但它们不仅仅是存根。
模拟:帮助模拟和检查结果交互。这些交互是 SUT 对其依赖项进行的调用以更改其状态。
存根:帮助模拟传入的交互。这些交互是 SUT 对其依赖项进行的调用以获取输入数据。
https://i.stack.imgur.com/jGhHA.png
来源:单元测试原则、实践和模式——曼宁
我看到了 UncleBob The Little Mocker 的这篇有趣的文章。它以非常易于理解的方式解释了所有术语,因此对初学者很有用。 Martin Fowlers 的文章很难读,尤其是对于像我这样的初学者。
那里有很多有效的答案,但我认为值得一提的是鲍勃叔叔的这张表格:https://8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html
有史以来最好的解释!
模拟既是技术对象又是功能对象。
模拟是技术性的。由于字节码生成,它确实是由模拟库(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;
}
}
其他情况:如果我有一个副作用方法(不返回)并且我会检查该方法是否被调用,我可能应该在存根类中添加一个布尔值或计数器来计算调用该方法的次数。
结论
存根通常需要大量开销/代码来为您的单元测试编写。由于提供了开箱即用的记录/验证功能,mock 可以防止什么。这就是为什么现在随着优秀的模拟库的出现,存根方法在实践中很少使用。
关于 Martin Fowler 文章:当我使用 mock 并且避免使用 stub 时,我不认为自己是一个“mockist”程序员。但是我在真正需要时使用模拟(烦人的依赖项),并且当我测试一个具有依赖项的类时,我更喜欢测试切片和迷你集成测试,而模拟将是一种开销。
加上有用的答案,使用 Mocks 比 Subs 最强大的一点之一
如果协作者[主要代码依赖它]不受我们控制(例如来自第三方库),在这种情况下,存根比模拟更难编写。
存根帮助我们运行测试。如何?它提供了有助于运行测试的值。这些值本身不是真实的,我们创建这些值只是为了运行测试。例如,我们创建一个 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()
{
// 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";
}
}
}
我在回答中使用了 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"
模拟对象用于模拟测试用例,它们验证在这些对象上调用了某些方法。模拟对象是以受控方式模仿真实对象行为的模拟对象。您通常会创建一个模拟对象来测试其他对象的行为。 Mocks 让我们可以模拟不可用或难以进行单元测试的资源。
我的模块.py:
import os
import os.path
def rm(filename):
if os.path.isfile(filename):
os.remove(filename)
测试.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 install mock) 下载。
我还阅读了 Roy Osherove 的“单元测试的艺术”,我认为如果使用 Python 和 Python 示例编写类似的书会很棒。如果有人知道这样的书,请分享。干杯:)