我已经阅读了各种关于测试中模拟与存根的文章,包括Martin Fowler 的 Mocks Aren't Stubs,但仍然不明白其中的区别。
40 回答
前言
对象有几种定义,它们是不真实的。通用术语是test double。该术语包括: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
.
模拟和存根之间的相似之处
两者的目的都是为了消除对类或函数的所有依赖项的测试,以便您的测试在他们试图证明的内容上更加集中和简单。
存根是一个简单的假对象。它只是确保测试顺利运行。
模拟是更智能的存根。您验证您的测试通过它。
这是对每一个的描述,然后是真实世界的样本。
虚拟- 只是为了满足
API
.示例:如果您正在测试一个类的方法,该方法在构造函数中需要许多强制参数,而这对您的测试没有影响,那么您可以创建虚拟对象来创建类的新实例。
Fake - 创建一个可能依赖于某些外部基础设施的类的测试实现。(最好的做法是您的单元测试实际上并不与外部基础设施交互。)
示例:创建用于访问数据库的假实现,将其替换为
in-memory
集合。存根- 覆盖方法以返回硬编码值,也称为
state-based
.示例:您的测试类依赖于一个
Calculate()
需要 5 分钟才能完成的方法。您可以用返回硬编码值的存根替换它的实际实现,而不是等待 5 分钟;只占用一小部分时间。模拟- 非常类似于
Stub
但interaction-based
不是基于状态的。这意味着您不期望 fromMock
返回某个值,而是假设进行了特定的方法调用顺序。示例:您正在测试用户注册类。调用后
Save
,应该调用SendConfirmationEmail
。
Stubs
并且Mocks
实际上是 的子类型Mock
,两者都将实际实现与测试实现交换,但出于不同的具体原因。
在codeschool.com课程Rails Testing for Zombies中,他们给出了以下术语的定义:
存根
用于用返回指定结果的代码替换方法。
嘲笑
带有方法被调用的断言的存根。
因此,正如 Sean Copenhaver 在他的回答中所描述的那样,不同之处在于模拟设定了期望(即断言,关于它们是否或如何被调用)。
存根不会使您的测试失败,模拟可以。
看了上面所有的解释,让我试着浓缩一下:
- Stub:一段让测试运行的虚拟代码,但你不关心它会发生什么。
- Mock:一段虚拟代码,您 VERIFY 作为测试的一部分被正确调用。
- Spy:一段虚拟代码,拦截对真实代码的一些调用,允许您在不替换整个原始对象的情况下验证调用。
我认为Roy Osherove在他的书The art of Unit Testing(第 85 页)中给出了关于这个问题的最简单和更清晰的答案
判断我们正在处理存根的最简单方法是注意存根永远不会通过测试。测试使用的断言总是针对被测类。
另一方面,测试将使用模拟对象来验证测试是否失败。[...]
同样,模拟对象是我们用来查看测试是否失败的对象。
存根和模拟都是假的。
如果您对假货进行断言,则意味着您将假货用作模拟,如果您仅使用假货来运行测试而不对其进行断言,则您将假货用作存根。
Mock 只是测试行为,确保调用某些方法。存根是特定对象的可测试版本(本身)。
苹果的方式是什么意思?
使用心智模型确实帮助我理解了这一点,而不是所有的解释和文章,这些解释和文章并没有完全“深入”。
想象一下,您的孩子在桌子上有一个玻璃盘子,他开始玩它。现在,您担心它会破裂。所以,你给他一个塑料盘子。那将是一个Mock(相同的行为,相同的接口,“更软”的实现)。
现在,假设你没有塑料替代品,所以你解释说“如果你继续玩它,它会坏掉的!”。那是一个Stub,你提前提供了一个预定义的状态。
一个Dummy将是他甚至没有使用的叉子......而Spy可能就像提供您已经使用过的相同解释一样有效。
如果将其与调试进行比较:
存根就像确保方法返回正确的值
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:Mocks 是注册它们收到的调用的对象。在测试断言中,我们可以在 Mocks 上验证所有预期的操作都已执行。如:调用电子邮件发送服务的功能。有关更多信息,请检查此。
我认为他们之间最重要的区别是他们的意图。
让我尝试在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
我喜欢 Roy Osherove [视频链接]的解释。
创建的每个类或对象都是假的。如果您验证对它的调用,它就是一个 Mock。否则它是一个存根。
- 存根与模拟
- 存根
- 为方法调用提供具体答案
- 例如: myStubbedService.getValues() 只返回被测代码所需的字符串
- 被测试代码用来隔离它
- 不能通过测试
- 例如: myStubbedService.getValues() 只返回存根值
- 经常实现抽象方法
- 为方法调用提供具体答案
- 模拟
- 存根的“超集”;可以断言某些方法被调用
- 例如:验证 myMockedService.getValues() 是否只被调用一次
- 用于测试被测代码的行为
- 可以通过测试
- 例如:验证 myMockedService.getValues() 是否被调用过一次;验证失败,因为我的测试代码没有调用 myMockedService.getValues()
- 经常模拟接口
- 存根的“超集”;可以断言某些方法被调用
- 存根
我正在阅读The Art of Unit Testing,偶然发现了以下定义:
fake是一个通用术语,可用于描述存根或模拟对象(手写或其他),因为它们看起来都像真实对象。fake 是 stub 还是 mock 取决于它在当前测试中的使用方式。如果它用于检查交互(断言反对),它是一个模拟对象。否则,它是一个存根。
赝品是一个通用术语,可用于描述存根或模拟对象(手写或其他),因为它们看起来都像真实对象。
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 类的结果。
存根
存根是用于伪造具有预编程行为的方法的对象。您可能希望使用它而不是现有方法以避免不必要的副作用(例如,存根可以进行虚假的 fetch 调用,该调用返回预编程的响应,而无需实际向服务器发出请求)。
嘲笑
模拟是用于伪造具有预编程行为以及预编程期望的方法的对象。如果未满足这些期望,则模拟将导致测试失败(例如,模拟可能会进行虚假的 fetch 调用,该调用会返回预编程的响应,而无需实际向服务器发出请求,例如第一个参数是"http://localhost:3008/"
其他情况测试会失败。)
区别
与模拟不同,存根没有预先编程的期望,可能会使您的测试失败。
来自 jMock 开发人员的论文Mock Roles, not Objects:
存根是返回预设结果的生产代码的虚拟实现。模拟对象充当存根,但也包括断言以检测目标对象与其邻居的交互。
因此,主要区别在于:
- 在存根上设置的期望通常是通用的,而在模拟上设置的期望可能更“聪明”(例如,在第一次调用时返回 this,在第二次调用时返回 this 等)。
- 存根主要用于设置 SUT 的间接输入,而模拟可用于测试SUT 的间接输入和间接输出。
总而言之,同时也试图消除Fowler 文章标题中的混淆:模拟是存根,但它们不仅仅是存根。
他使用的通用术语是测试替身(想想特技替身)。Test Double 是一个通用术语,用于替换生产对象以进行测试的任何情况。Gerard 列出了多种类型的 double:
- 虚拟对象被传递但从未实际使用过。通常它们仅用于填充参数列表。
- 假对象实际上有工作实现,但通常采取一些捷径,这使得它们不适合生产(InMemoryTestDatabase 就是一个很好的例子)。
- 存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。
- 间谍是存根,它还根据调用方式记录一些信息。其中一种形式可能是电子邮件服务,它记录发送了多少消息(也称为Partial Mock)。
- 模拟预编程了期望,这些期望形成了他们期望接收的调用的规范。如果他们收到了他们不期望的呼叫,他们可以抛出异常,并在验证过程中进行检查以确保他们得到了他们期望的所有呼叫。
我看到了 UncleBob The Little Mocker 的这篇有趣的文章。它以非常易于理解的方式解释了所有术语,因此对初学者很有用。Martin Fowlers 的文章很难读,尤其是对于像我这样的初学者。
模拟既是技术对象又是功能对象。
模拟是技术性的。由于字节码生成,它确实是由模拟库(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”程序员。
但是我在真正需要时使用模拟(烦人的依赖项),并且当我测试一个具有依赖项的类时,我更喜欢测试切片和迷你集成测试,而模拟将是一种开销。
那里有很多有效的答案,但我认为值得一提的是鲍勃叔叔的这张表格: https ://8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html
有史以来最好的解释!
加上有用的答案,使用 Mocks 比 Subs最强大的一点之一
如果协作者[主要代码依赖它]不受我们控制(例如来自第三方库),
在这种情况下,stub 比 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()
{
// 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 mock 中的注释不包含在 unittest 中,而是一个可下载的模块,可以通过 pip (pip install mock) 下载。
我还阅读了 Roy Osherove 的“单元测试的艺术”,我认为如果使用 Python 和 Python 示例编写类似的书会很棒。如果有人知道这样的书,请分享。干杯:)
存根是向 SUT 返回值的测试替身。
模拟是测试用于验证 SUT 是否正确调用依赖项的测试替身。
此外,模拟通常是存根
存根是一个空函数,用于在测试期间避免未处理的异常:
function foo(){}
模拟是一种人工函数,用于在测试期间避免操作系统、环境或硬件依赖性:
function foo(bar){ window = this; return window.toString(bar); }
在断言和状态方面:
- 在事件或状态更改之前断言模拟
- 存根没有被断言,它们在事件之前提供状态以避免从不相关的单元执行代码
- 间谍像存根一样设置,然后在事件或状态更改后断言
- 假货没有被断言,它们在具有硬编码依赖项的事件之后运行以避免状态
参考
假设您有一个要测试的名为 EmployeeService 的类,并且它对名为 EmployeeDao 的接口有一个依赖项:
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
@Before
public void setUp(){
service = new EmployeeService(mockDao);
}
//Tests
//....
}
在上面第 3 行的测试类中,我们对模拟框架(在本例中为 Mockito)说:“嘿,Mockito,为我制作一个具有 EmployeeDao 功能的对象。” 框架将创建一个具有方法goToDatabaseAndBringTheEmployeeWithId
但实际上没有主体的对象。你的工作是指导那个模拟做什么。这是一个模拟。
但是您也可以创建一个实现 EmployeeDao 接口的类,并在测试类中使用它:
public EmployeeDaoStub implements EmployeeDao{
public Employee goToDatabaseAndBringTheEmployeeWithId(int id){
//No trip to DB, just returning a dummy Employee object
return new Employee("John","Woo","123 Lincoln str");
}
}
这次在您的测试类中使用存根而不是模拟:
public class EmployeeServiceTest{
EmployeeService service;
EmployeeDao daoStub = new EmployeeDaoStub();//Line 3
@Before
public void setUp(){
service = new EmployeeService(daoStub);
}
//Tests
//....
}
因此,总而言之,存根是您创建(或其他人创建)的类,专门用于模仿某些依赖项,只是为了获得所需的状态。是的,正如所有其他人所说,它主要是关于一个状态而模拟通常是由一个模拟框架创建的,你不知道它的胆量是什么样的。但是有了存根,你就知道你会得到什么类:它是你创建的。
哦,顺便说一句,如果你的依赖是一个类而不是一个接口,你可以扩展那个类来创建你的存根。
存根是为测试目的而构建的假对象。模拟是记录预期调用是否有效发生的存根。
存根是实现组件接口的对象,但不是返回组件在调用时返回的内容,而是可以将存根配置为返回适合测试的值。使用存根,单元测试可以测试一个单元是否可以处理来自其协作者的各种返回值。在单元测试中使用存根而不是真正的协作者可以这样表示:
单元测试 --> 存根
单元测试 --> 单元 --> 存根
单元测试断言单元的结果和状态
首先,单元测试创建存根并配置其返回值。然后单元测试创建单元并在其上设置存根。现在单元测试调用单元,该单元又调用存根。最后,单元测试对单元上方法调用的结果做出断言。
Mock 就像一个存根,只是它还有一些方法可以确定在 Mock 上调用了哪些方法。因此,使用模拟可以测试该单元是否可以正确处理各种返回值,以及该单元是否正确使用协作者。例如,您无法通过从 dao 对象返回的值来查看数据是使用 Statement 还是 PreparedStatement 从数据库中读取的。您也看不到在返回值之前是否调用了 connection.close() 方法。这可以通过模拟来实现。换句话说,模拟使测试单元与协作者的完整交互成为可能。不仅仅是返回单元使用的值的协作者方法。在单元测试中使用模拟可以这样表达:
单元测试 --> 模拟
单元测试 --> 单元 --> 模拟
单元测试断言单元的结果和状态
单元测试断言在模拟上调用的方法
更多详情>>这里
测试对象响应某些提示(函数调用)或其他刺激执行动作。以下是测试情况的具体示例。
情景——EMT学生考试
一名学生学习成为一名紧急医疗技术员。如果您不熟悉这种测试情况,请去看《无耻的第 6 季第 10 集》中的 Ian Gallagher。
为测试目的寻找患有各种疾病的患者太昂贵了。相反,我们使用演员。我们问测试对象(伊恩)“你到达现场,病人被固定且失去知觉,你首先做什么?” 伊恩回答“我检查现场是否安全”。并且测试指导员说“现场是安全的”。
讲师(和演员)能够为测试对象的查询注入任意答案。
在这里,教练(和演员)是一个模拟者。医学培训与计算机科学家一样使用这个术语(例如模拟代码模拟)。
场景——注册一个网站
您正在测试雅虎,这是您听说过的一种新的电子邮件服务。为了注册,您必须提供您的生日和其他侵入性问题的答案。
该网站要求您年满 21 岁。因此,您输入值 1970 年 1 月 1 日。它符合要求,并且使您免于实施记住我的生日并输入工作流的繁琐过程。
这个日期是一个存根。这个词的用法是特定于计算机科学的。
Stubs 和 Mocks 都覆盖了外部依赖项,但区别在于
存根->测试数据
模拟->测试行为
Fake/Dummy ->不进行任何测试(只需使用空方法覆盖功能,例如替换Logger
以避免测试时出现任何日志记录噪音)
以下是我的理解...
如果您在本地创建测试对象并以此为本地服务提供服务,那么您正在使用模拟对象。这将对您在本地服务中实现的方法进行测试。它用于验证行为
当您从真实的服务提供者那里获得测试数据时,虽然从接口的测试版本并获得对象的测试版本,但您正在使用存根,存根可以有逻辑来接受某些输入并给出相应的输出来帮助您执行状态验证...
存根用于具有您在测试中设置的预期返回值的方法。模拟用于 void 方法,这些方法在 Assert 中验证它们被调用。
模拟- 模拟拦截对方法或函数(或一组方法和函数,如模拟类的情况)的调用。它不是该方法或功能的替代品。在那个拦截中,mock 可以做它想做的任何事情,比如记录输入和输出,决定短路调用,改变返回值等等。
存根- 存根是方法或函数(或方法和函数组,如在存根类的情况下)的有效完整工作实现,它与方法、函数或方法组和函数具有相同的接口/签名正在为。存根实现通常只会做在单元测试上下文中可接受的事情,这意味着它不会做 IO,例如,同时模仿它正在存根的事物的行为。