我知道我如何使用这些术语,但我想知道是否存在伪造,模拟和存根的可接受定义单元测试?你如何为你的测试定义这些?描述您可能使用每种情况的情况。
以下是我如何使用它们:
假:实现接口但包含固定数据且没有逻辑的类。根据实施情况,简单地返回“好”或“坏”数据。
模拟:一个实现接口的类,允许动态设置值以返回/异常从特定方法抛出,并提供检查特定方法是否被调用的能力调用。
存根:类似于模拟类,但它不提供验证方法是否已被调用/未调用的能力。
模拟和存根可以由模拟框架手动生成或生成。伪造的类是手工生成的。我主要使用模拟来验证我的类和依赖类之间的交互。一旦我验证了交互并且正在通过我的代码测试备用路径,我就会使用存根。我主要使用假类来抽象出数据依赖性,或者每次使用模拟/存根都太繁琐而无法设置。
答案 0 :(得分:476)
您可以获得一些信息:
伪对象实际上有工作实现,但通常采取一些使其不适合生产的快捷方式
存根为测试期间发出的呼叫提供固定答案,通常根本不会对测试中编程的任何内容做出任何响应。存根也可以记录有关呼叫的信息,例如记住它'发送'的消息的电子邮件网关存根,或者可能只记录它'发送'的消息。
模拟是我们在这里讨论的内容:预先编程了预期的对象,这些对象形成了预期会收到的呼叫规范。
假:我们获取或构建一个非常轻量级的实现,其功能与SUT所依赖的组件提供的功能相同,并指示SUT使用它而不是真实的。
存根:此实现配置为响应来自SUT的调用,其中包含将执行SUT内未测试代码(请参阅第X页上的生产错误)的值(或例外)。使用测试存根的关键指示是由于无法控制SUT的间接输入而导致的未测试代码
模拟对象,它实现与SUT(被测系统)所依赖的对象相同的接口。当我们需要进行行为验证时,我们可以使用Mock对象作为观察点,以避免因无法观察SUT上调用方法的副作用而导致未经测试的要求(请参阅第X页的生产错误)。
我尝试使用:Mock和Stub进行简化。当它是一个返回一个设置为测试类的值的对象时,我使用Mock。我使用Stub来模拟要测试的Interface或Abstract类。事实上,你所谓的它并不重要,它们都是未在生产中使用的类,并且被用作测试的实用程序类。
答案 1 :(得分:178)
存根 - 提供方法调用的预定义答案的对象。
模拟 - 您设定期望的对象。
假 - 具有有限功能的对象(用于测试目的),例如假的网络服务。
Test Double是存根,模拟和假货的总称。但非正式地说,你经常听到人们只是称他们为嘲笑。
答案 2 :(得分:86)
我很惊讶这个问题已经存在了很长时间,而且还没有人提供基于Roy Osherove's "The Art of Unit Testing"的答案。
In" 3.1介绍存根"将存根定义为:
存根是现有依赖关系的可控替代 (或合作者)在系统中。通过使用存根,您可以在不使用的情况下测试代码 直接处理依赖。
并将存根和模拟之间的区别定义为:
要记住关于mocks和stubs的主要事情是模拟就像存根一样,但是你对模拟对象断言,而你没有断言存根。
Fake只是用于存根和模拟的名称。例如,当你不关心存根和模拟之间的区别时。
Osherove区分存根和模拟的方式意味着任何用作测试假的类都可以是存根或模拟。对于特定测试而言,完全取决于您在测试中编写检查的方式。
将FakeX类用作存根的测试示例:
const pleaseReturn5 = 5;
var fake = new FakeX(pleaseReturn5);
var cut = new ClassUnderTest(fake);
cut.SquareIt;
Assert.AreEqual(25, cut.SomeProperty);
fake
实例用作存根,因为Assert
根本不使用fake
。
将测试类X用作模拟的测试示例:
const pleaseReturn5 = 5;
var fake = new FakeX(pleaseReturn5);
var cut = new ClassUnderTest(fake);
cut.SquareIt;
Assert.AreEqual(25, fake.SomeProperty);
在这种情况下,Assert
会检查fake
上的值,使该假货成为模拟。
现在,这些例子当然是非常人为的,但我认为这种区别很有用。它让您了解如何测试您的内容以及测试的依赖性。
我同意Osherove的那个
从纯可维护性的角度来看,在我使用模拟的测试中,比不使用它们会带来更多麻烦。这是我的经历,但我总是在学习新的东西。
断言假冒是你真正想避免的事情,因为它会使你的测试高度依赖于一个完全没有被测试的类的实现。这意味着类ActualClassUnderTest
的测试可能会因为ClassUsedAsMock
的实现发生变化而开始破坏。这给我带来了难闻的气味。 ActualClassUnderTest
的测试最好只在ActualClassUnderTest
更改时中断。
我意识到写假冒伪劣是一种常见的做法,特别是当你是一个模仿者类型的TDD用户时。我想我肯定与古典主义阵营中的Martin Fowler(见Martin Fowler's "Mocks aren't Stubs")并且像Osherove一样尽可能避免交互测试(这只能通过断言来假装)。
为了有趣地阅读为什么你应该避免这里定义的模拟,google for" fowler mockist classicist"。你会发现过多的意见。
答案 3 :(得分:7)
为了说明存根和模拟的用法,我想提供一个基于Roy Osherove" The Art of Unit Testing"的例子。
想象一下,我们有一个LogAnalyzer应用程序,它具有打印日志的唯一功能。它不仅需要与Web服务通信,而且如果Web服务引发错误,LogAnalyzer必须将错误记录到不同的外部依赖项,通过电子邮件将其发送给Web服务管理员。
这是我们想在LogAnalyzer中测试的逻辑:
if(fileName.Length<8)
{
try
{
service.LogError("Filename too short:" + fileName);
}
catch (Exception e)
{
email.SendEmail("a","subject",e.Message);
}
}
当Web服务抛出异常时,如何测试LogAnalyzer是否正确调用电子邮件服务? 以下是我们面临的问题:
我们如何更换网络服务?
我们如何模拟Web服务中的异常,以便我们可以 测试电话服务的电话?
我们如何知道电子邮件服务是正确调用还是在 所有
我们可以通过使用网络服务存根处理前两个问题。要解决第三个问题,我们可以使用模拟对象进行电子邮件服务。
假是一个通用术语,可用于描述存根或模拟。在我们的测试中,我们将有两个假货。一个是电子邮件服务模拟,我们将用它来验证是否已将正确的参数发送到电子邮件服务。另一个将是一个存根,我们将用它来模拟从Web服务抛出的异常。这是一个存根,因为我们不会使用Web服务假来验证测试结果,只是为了确保测试正确运行。电子邮件服务是一个模拟,因为我们会断言它被正确调用。
[TestFixture]
public class LogAnalyzer2Tests
{
[Test]
public void Analyze_WebServiceThrows_SendsEmail()
{
StubService stubService = new StubService();
stubService.ToThrow= new Exception("fake exception");
MockEmailService mockEmail = new MockEmailService();
LogAnalyzer2 log = new LogAnalyzer2();
log.Service = stubService
log.Email=mockEmail;
string tooShortFileName="abc.ext";
log.Analyze(tooShortFileName);
Assert.AreEqual("a",mockEmail.To); //MOCKING USED
Assert.AreEqual("fake exception",mockEmail.Body); //MOCKING USED
Assert.AreEqual("subject",mockEmail.Subject);
}
}
答案 4 :(得分:5)
这是一个让测试富有表现力的问题。如果我想让测试描述两个对象之间的关系,我会对模拟设置期望。如果我正在设置支持对象以使我了解测试中的有趣行为,那么我会返回值。
答案 5 :(得分:5)
正如投票最多的答案所提到的,马丁·福勒(Martin Fowler)在Mocks Aren't Stubs中讨论了这些区别,尤其是在副标题The Difference Between Mocks and Stubs中,因此请务必阅读该文章。
我认为专注于为什么是不同的概念,而不是专注于这些东西有什么不同,而不是关注它们。每个存在的目的都是不同的。
伪造是行为“自然”但不是“真实”的实现。这些都是模糊的概念,因此不同的人对使事物成为假冒品的理解不同。
一个伪造的例子是内存数据库(例如,将sqlite与:memory:
存储一起使用)。您永远不会将其用于生产(因为数据不会持久保存),但是它完全可以用作测试环境中的数据库。它也比“真实”数据库轻巧得多。
再举一个例子,也许您在生产中使用了某种对象存储(例如Amazon S3),但是在测试中,您可以简单地将对象保存到磁盘上的文件中。那么您的“保存到磁盘”实现将是假的。 (或者您甚至可以通过使用内存文件系统来伪造“保存到磁盘”操作。)
作为第三个示例,想象一个提供缓存API的对象;一个实现了正确接口但根本不执行缓存但始终返回缓存未命中的对象将是一种伪造。
伪造的目的不是 影响被测系统的行为,而是简化测试的实现 (通过删除不必要的或重量级的依赖项)。
存根是行为“异常”的实现。它已预先配置(通常是通过测试设置)以响应具有特定输出的特定输入。
存根的目的是使被测系统进入特定状态。例如,如果您正在为与REST API交互的某些代码编写测试,则可以使用所有始终返回固定响应或以特定错误响应API请求的API的REST API。这样,您可以编写测试来断言系统对这些状态的反应。例如,如果API返回404错误,则测试用户获得的响应。
存根通常实现为仅响应您告诉它要响应的确切交互。但是,使存根变成某些东西的关键功能是它的用途:存根就是建立测试用例。
模拟类似于存根,但其中添加了 verification 。模拟的目的是断定被测系统如何交互具有依赖性。
例如,如果要编写一个将文件上传到网站的系统的测试,则可以构建一个 mock 来接受一个文件,并且可以用来断言上传的文件是正确。或者,在较小的规模上,通常使用对象的模拟来验证被测系统是否调用了模拟对象的特定方法。
模仿是与交互测试绑定的,后者是一种特定的测试方法。宁愿测试系统状态而不是系统交互的人也会很少使用模拟程序。
假货,存根和模拟都属于 test doubles 的类别。测试倍数是您在测试中代替而不是使用的任何对象或系统。大多数自动化软件测试都使用某种或多种测试倍数。其他一些测试双打类型包括虚拟值,间谍和I / O 黑洞。
答案 6 :(得分:3)
如果您熟悉Arrange-Act-Assert,那么解释存根和模拟之间可能对您有用的区别的一种方法是,存根属于排列部分,因为它们用于排列输入状态,并且模拟属于断言部分,因为它们用于声明结果。
假人不做任何事。它们只是用于填充参数列表,因此您不会得到未定义或null错误。它们也存在以满足严格类型语言中的类型检查器,因此您可以被允许编译和运行。
答案 7 :(得分:3)
Unit testing
-是一种在单元(类,方法)受到控制的情况下进行测试的方法。
Test double
-不是主要对象(来自OOP世界)。它是临时创建的用于测试,检查或在开发期间的实现。测试双打类型:
fake object
是interface(protocol)或扩展的真实实现,它使用继承或其他可用于创建的方法- is
依赖性。通常,它是由开发人员创建的最简单的解决方案,用来替代某些依赖项
stub object
是具有额外状态的裸对象(0,nil和无逻辑的方法),已由开发人员预定义以定义返回值。通常是由框架创建的
mock object
与stub object
非常相似,但是在程序执行期间更改了额外状态来检查是否发生了某些事情(调用了方法)。
spy object
是带有“部分嘲笑”的真实对象。这意味着您要使用非双重对象,但模拟行为除外
dummy object
是运行测试所必需的对象,但是不会调用该对象的任何变量或方法。
答案 8 :(得分:2)
您在其上断言的东西称为 mock 对象,而其他仅有助于测试运行的内容是 stub 。
答案 9 :(得分:2)
存根,伪造品和假货在不同来源具有不同的含义。我建议您介绍团队内部条款并同意其含义。
我认为区分两种方法很重要: -行为验证(暗示行为替代) -最终状态验证(暗示行为仿真)
如果发生错误,请考虑发送电子邮件。进行行为验证时-检查Send
中的方法IEmailSender
已执行一次。并且您需要模拟此方法的返回结果,返回已发送消息的ID。所以你说:“我希望Send
被调用。对于任何调用,我只会返回虚拟(或随机)ID”。。这是行为验证:
emailSender.Expect(es=>es.Send(anyThing)).Return((subject,body) => "dummyId")
进行状态验证时,您需要创建实现TestEmailSender
的{{1}}。并实施IEmailSender
方法-通过将输入保存到某些数据结构中,以用于将来的状态验证,例如某些对象的数组Send
,然后测试您将检查SentEmails
是否包含预期的电子邮件。这是状态验证:
SentEmails
从我的阅读中,我了解到行为验证通常称为模拟。 而状态验证通常称为存根或假冒。
答案 10 :(得分:1)
答案 11 :(得分:0)
存根和伪造是对象,它们可以根据输入参数改变其响应。它们之间的主要区别是Fake比存根更接近真实世界的实现。存根基本上包含对预期请求的硬编码响应。让我们看一个例子:
public class MyUnitTest {
@Test
public void testConcatenate() {
StubDependency stubDependency = new StubDependency();
int result = stubDependency.toNumber("one", "two");
assertEquals("onetwo", result);
}
}
public class StubDependency() {
public int toNumber(string param) {
if (param == “one”) {
return 1;
}
if (param == “two”) {
return 2;
}
}
}
模拟是假货和存根的升级。模拟提供与存根相同的功能,但更为复杂。他们可以为他们定义规则,这些规则规定必须调用API上的方法的顺序。大多数模拟都可以跟踪方法被调用的次数,并可以根据该信息做出反应。嘲笑通常知道每个呼叫的上下文,并且在不同情况下可以做出不同的反应。因此,模拟需要他们正在模拟的类的一些知识。存根通常无法跟踪方法的调用次数或方法序列的调用顺序。一个模拟看起来像:
public class MockADependency {
private int ShouldCallTwice;
private boolean ShouldCallAtEnd;
private boolean ShouldCallFirst;
public int StringToInteger(String s) {
if (s == "abc") {
return 1;
}
if (s == "xyz") {
return 2;
}
return 0;
}
public void ShouldCallFirst() {
if ((ShouldCallTwice > 0) || ShouldCallAtEnd)
throw new AssertionException("ShouldCallFirst not first thod called");
ShouldCallFirst = true;
}
public int ShouldCallTwice(string s) {
if (!ShouldCallFirst)
throw new AssertionException("ShouldCallTwice called before ShouldCallFirst");
if (ShouldCallAtEnd)
throw new AssertionException("ShouldCallTwice called after ShouldCallAtEnd");
if (ShouldCallTwice >= 2)
throw new AssertionException("ShouldCallTwice called more than twice");
ShouldCallTwice++;
return StringToInteger(s);
}
public void ShouldCallAtEnd() {
if (!ShouldCallFirst)
throw new AssertionException("ShouldCallAtEnd called before ShouldCallFirst");
if (ShouldCallTwice != 2) throw new AssertionException("ShouldCallTwice not called twice");
ShouldCallAtEnd = true;
}
}
答案 12 :(得分:0)
我从以下资源中学到了很多东西,并得到了罗伯特·C·马丁(鲍勃叔叔)的出色解释:
它解释了
的区别和精妙之处它还提到了Martin Fowler,并解释了一些软件测试历史。
我绝不打算通过此链接回答这个问题,这是真正的答案。但是,它帮助我更好地理解了模拟和间谍的概念,因此我希望能够为更多的人提供帮助。
答案 13 :(得分:0)
在 Gerard Meszaros 的 xUnit Test Patterns 书中有一个很好的表格,可以很好地洞察差异