我已经阅读了有关测试中的模拟与存根的各种文章,包括Martin Fowler的Mocks Are n't Stubs,但仍然不了解它们之间的区别。
我已经阅读了有关测试中的模拟与存根的各种文章,包括Martin Fowler的Mocks Are n't Stubs,但仍然不了解它们之间的区别。
Answers:
存根
我相信最大的区别是您已经以预定的行为编写了存根。因此,您将拥有一个类,该类实现您为测试目的而伪装的依赖项(最有可能是抽象类或接口),并且该方法将仅通过设置的响应进行处理。他们不会做任何花哨的事情,并且您已经在测试之外为其编写了存根代码。
嘲笑
模拟是在测试过程中必须设置的期望值。模拟不是以预定的方式设置的,因此您具有在测试中执行该模拟的代码。嘲笑是在运行时确定的,因为设置期望的代码必须在它们执行任何操作之前运行。
存根和存根之间的区别
用模拟编写的测试通常遵循一种initialize -> set expectations -> exercise -> verify
测试模式。而预写的存根将跟随一个initialize -> exercise -> verify
。
存根和存根之间的相似性
两者的目的都是消除测试一个类或函数的所有依赖关系,以便您的测试在尝试证明时更加专注和简单。
对象有几种定义,它们不是真实的。通用词是test double。该术语包括:虚拟,伪造,存根,模拟。
根据马丁·福勒的文章:
- 虚拟对象会传递,但从未实际使用过。通常它们仅用于填充参数列表。
- 伪对象实际上具有有效的实现,但是通常采取一些捷径,这使其不适合生产(内存数据库是一个很好的示例)。
- 存根提供对测试过程中进行的呼叫的固定答复,通常不响应为测试编程的内容之外的任何内容。存根还可以记录有关呼叫的信息,例如电子邮件网关存根,它可以记住“已发送”的消息,或者仅记住“已发送”的消息数量。
- 嘲笑是我们在这里谈论的内容:对象被预先编程并带有期望,形成了期望接收的呼叫的规范。
嘲弄与存根=行为测试与状态测试
根据测试的原理,每个测试只有一件事,一个测试中可能有多个存根,但通常只有一个模拟。
使用存根测试生命周期:
使用模拟测试生命周期:
模拟和存根测试都为以下问题提供了答案:结果如何?
使用模拟进行测试也感兴趣:如何获得结果?
存根是一个简单的伪造对象。它只是确保测试顺利进行。
模拟是更聪明的存根。您验证您的测试通过了。
这是每个示例的说明,后面是真实示例。
虚拟 -只是虚假的价值观来满足API
。
示例:如果要测试的类的方法在构造函数中需要许多强制性参数,而这些参数对测试没有影响,则可以创建虚拟对象以创建类的新实例。
伪造 -创建一个类的测试实现,该类可能依赖于某些外部基础结构。(优良作法是您的单元测试实际上不与外部基础结构交互。)
示例:创建用于访问数据库的伪造实现,将其替换为
in-memory
collection。
存根重写方法可返回硬编码的值,也称为state-based
。
示例:您的测试课程取决于
Calculate()
需要5分钟才能完成的方法。无需等待5分钟,您可以用返回硬编码值的存根替换其实际实现。仅花费一小部分时间。
模拟 -非常类似于Stub
但interaction-based
不是基于状态的。这意味着您不希望from Mock
从中返回任何值,而是假设已完成特定的方法调用顺序。
示例:您正在测试用户注册类。打电话后
Save
,应该打电话SendConfirmationEmail
。
Stubs
和Mocks
实际上是的子类型Mock
,都将实际实现与测试实现互换,但是出于不同的特定原因。
在codeschool.com课程“僵尸的Rails测试”中,他们给出了以下术语的定义:
存根
用返回指定结果的代码替换方法。
嘲笑
带有断言该方法被调用的存根。
因此,正如肖恩·哥本哈根(Sean Copenhaver)在回答中所描述的那样,区别在于嘲笑设定了期望(即,对它们是否被调用或如何被调用进行断言)。
存根不会使您的测试失败,模拟可以。
我认为Roy Osherove在他的书《单元测试的艺术》(第85页)中给出了关于这个问题的最简单,更清晰的答案。
告诉我们正在处理存根的最简单方法是注意到存根永远不会通过测试。测试使用的断言始终与被测类相对。
另一方面,测试将使用模拟对象来验证测试是否失败。[...]
同样,模拟对象是我们用来查看测试是否失败的对象。
这意味着,如果您对假货进行断言,则意味着您将假货用作模拟,如果仅使用假货来运行测试而没有断言,则将假货用作存根。
模拟只是测试行为,确保调用了某些方法。存根是特定对象的可测试版本(本身)。
你用苹果的方式是什么意思?
我认为他们之间最重要的区别是他们的意图。
让我尝试在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
通过编写模拟,您可以通过验证是否满足期望来发现对象协作关系,而存根仅模拟对象的行为。
如果您想进一步了解模拟,建议阅读这篇文章:http : //jmock.org/oopsla2004.pdf
要非常清楚和实用:
存根(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!');
}
}
让我们来看看测试双打:
存根(Stub):存根是一个对象,用于保存预定义的数据,并在测试期间将其用于应答呼叫。如:需要从数据库中获取一些数据以响应方法调用的对象。
嘲笑:嘲笑是注册收到的呼叫的对象。在测试断言中,我们可以在Mocks上验证是否已执行所有预期的操作。如:调用电子邮件发送服务的功能。要了解更多,只需检查一下。
一个假的是可以用来描述任何存根或模拟对象(手写或其他方式)的总称,因为他们看起来像真正的对象。
假货是存根还是假货,取决于当前测试中的使用方式。如果用于检查交互(认定为无效),则它是一个模拟对象。否则,它是一个存根。
伪造品可确保测试顺利进行。这意味着您将来的测试的读者将了解假对象的行为,而无需读取其源代码(而无需依赖外部资源)。
测试顺利进行意味着什么?
例如下面的代码:
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类的结果。
由jMock的开发人员在论文《模拟角色而不是对象》中提出:
存根是返回固定结果的生产代码的虚拟实现。模拟对象充当存根,但还包括断言以检测目标对象与其邻居的交互。
因此,主要区别是:
总而言之,同时还试图消除Fowler文章标题中的困惑:模拟是存根,但它们不仅是存根。
我在阅读《单元测试的艺术》,偶然发现了以下定义:
一个假是可以用来描述任何存根或模拟对象(手写或其他方式)的总称,因为他们看起来像真正的对象。假货是存根还是假货,取决于当前测试中的使用方式。如果用于检查交互作用(认定为无效),则为模拟对象。否则,它是一个存根。
存根可以帮助我们进行测试。怎么样?它提供有助于运行测试的值。这些值本身不是真实的,我们创建这些值只是为了运行测试。例如,我们创建一个HashMap来给我们提供类似于数据库表中值的值。因此,我们不是直接与数据库进行交互,而是与Hashmap进行交互。
模拟是运行测试的伪造对象。我们把断言放在哪里。
请参见下面使用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";
}
}
}
加上有用的答案,使用Mocks比Subs最强大的功能之一
如果协作者(主要代码依赖于它)不在我们的控制之下(例如,来自第三方库),那么
在这种情况下,存根比模拟更难编写。
我在答案中使用了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示例编写类似的书,那将是很棒的。如果有人知道这本书,请分享。干杯:)
存根是一个空函数,用于避免测试期间出现未处理的异常:
function foo(){}
模拟是一种人工函数,用于避免测试期间的操作系统,环境或硬件依赖性:
function foo(bar){ window = this; return window.toString(bar); }
在断言和状态方面:
参考文献
那里有很多有效的答案,但我认为值得一提的是鲍伯叔叔的这种形式: 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;
}
}
其他情况:如果我有一个副作用方法(没有返回值),并且要检查该方法是否已被调用,则可能应该在存根类中添加一个布尔值或计数器来计算该方法被调用的次数。
结论
存根通常需要很多开销/代码来编写单元测试。开箱即用的功能提供了录制/验证功能,从而阻止了模拟。
这就是为什么如今随着优秀的模拟库的出现,存根方法很少在实践中使用。
关于Martin Fowler的文章:当我使用模拟并且避免存根时,我不认为自己是“模拟主义者”程序员。
但是我在真正需要时使用了模拟(使依赖项烦恼),并且当我测试具有依赖项的类时,我更喜欢测试切片和小型集成测试,而模拟将是一项开销。