我知道我如何使用这些术语,但我想知道是否有公认的用于单元测试的伪造、模拟和存根定义?你如何为你的测试定义这些?描述您可能使用每种情况的情况。
以下是我如何使用它们:
Fake:实现了接口但包含固定数据且没有逻辑的类。根据实现简单地返回“好”或“坏”数据。
Mock:一个实现接口并允许动态设置返回值/异常以从特定方法抛出的能力的类,并提供检查特定方法是否已调用/未调用的能力。
存根:类似于模拟类,只是它不提供验证方法已被调用/未调用的能力。
模拟和存根可以手动生成或由模拟框架生成。假类是手工生成的。我主要使用模拟来验证我的类和依赖类之间的交互。一旦我验证了交互并通过我的代码测试备用路径,我就会使用存根。我主要使用假类来抽象出数据依赖关系,或者当模拟/存根太繁琐而无法每次设置时。
你可以得到一些信息:
来自 Martin Fowler 关于 Mock 和 Stub
假对象实际上有工作实现,但通常采取一些捷径,这使得它们不适合生产
存根为测试期间拨打的电话提供预设答案,通常根本不响应任何超出测试程序的内容。存根还可以记录有关呼叫的信息,例如记住它“发送”的消息的电子邮件网关存根,或者可能只记录它“发送”的消息的数量。
模拟就是我们在这里讨论的内容:预先编程的对象具有期望,这些期望形成了它们期望接收的调用的规范。
来自 xunitpattern:
Fake:我们获取或构建与 SUT 所依赖的组件提供的相同功能的非常轻量级的实现,并指示 SUT 使用它而不是真实的。
存根:此实现被配置为响应来自 SUT 的调用,其值(或异常)将在 SUT 中执行未经测试的代码(请参阅第 X 页的生产错误)。使用测试存根的一个关键指标是由于无法控制 SUT 的间接输入而导致出现未经测试的代码
模拟对象,它实现与 SUT(被测系统)所依赖的对象相同的接口。当我们需要进行行为验证时,我们可以使用模拟对象作为观察点,以避免由于无法观察 SUT 上调用方法的副作用而导致出现未经测试的需求(请参阅第 X 页的生产错误)。
亲自
我尝试通过使用来简化:Mock and Stub。当它是一个返回设置为测试类的值的对象时,我使用 Mock。我使用 Stub 来模拟要测试的接口或抽象类。事实上,你怎么称呼它并不重要,它们都是在生产中不使用的类,并且用作测试的实用程序类。
存根 - 为方法调用提供预定义答案的对象。
模拟 - 您设定期望的对象。
Fake - 具有有限功能的对象(用于测试目的),例如伪造的 Web 服务。
Test Double 是 stubs、mock 和 fakes 的总称。但非正式地,你会经常听到人们简单地称他们为嘲笑者。
EXPECT_CALL()
,通过使用 .WillOnce(Invoke(my_func_or_lambda_func))
(或使用 {5 }) 附加到 EXPECT_CALL()
的类型语法。在我的长答案的底部,可以在不同的上下文中看到使用 Invoke()
的一些示例:stackoverflow.com/a/60905880/4561887。
Invoke()
上的 Gmock 文档在这里:github.com/google/googletest/blob/master/googlemock/docs/…。无论如何,结论是:Google mock (gmock) 允许人们轻松地创建模拟 和 存根,尽管大多数模拟不是存根。
令我惊讶的是,这个问题已经存在了这么久,而且还没有人根据 Roy Osherove's "The Art of Unit Testing" 提供答案。
在“3.1 介绍存根”中将存根定义为:
存根是系统中现有依赖项(或协作者)的可控替换。通过使用存根,您可以在不直接处理依赖关系的情况下测试您的代码。
并将存根和模拟之间的区别定义为:
关于模拟与存根,要记住的主要一点是,模拟就像存根一样,但是你断言模拟对象,而你不断言存根。
Fake 只是用于存根和模拟的名称。例如,当您不关心存根和模拟之间的区别时。
Osherove 区分 stub 和 mock 的方式意味着,任何用作测试的假的类都可以是 stub 或 mock。它用于特定测试完全取决于您如何在测试中编写检查。
当您的测试检查被测类中的值时,或者实际上是除了假货之外的任何地方,假货被用作存根。它只是提供值供被测类使用,要么直接通过调用返回的值,要么间接通过调用它的结果(在某些状态下)引起副作用。
当您的测试检查假货的值时,它被用作模拟。
将 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
上的值,从而使该假冒成为模拟。
当然,这些例子是高度人为的,但我认为这种区别有很大的优点。它让你知道你是如何测试你的东西的,以及你的测试的依赖关系在哪里。
我同意奥舍罗夫的观点
从纯粹的可维护性的角度来看,在我的测试中使用模拟比不使用它们会产生更多的麻烦。这是我的经验,但我总是在学习新的东西。
反对虚假是你真正想要避免的事情,因为它使你的测试高度依赖于一个根本不是被测试的类的实现。这意味着类 ActualClassUnderTest
的测试可能会开始中断,因为 ClassUsedAsMock
的实现发生了变化。这给我带来了难闻的气味。对 ActualClassUnderTest
的测试最好只在 ActualClassUnderTest
更改时才中断。
我意识到针对虚假编写断言是一种常见的做法,尤其是当您是 TDD 订阅者的 mockist 类型时。我想我在古典主义阵营中坚定地支持 Martin Fowler(参见 Martin Fowler's "Mocks aren't Stubs"),并且像 Osherove 一样尽可能避免交互测试(这只能通过断言反对虚假来完成)。
为了有趣地阅读为什么你应该避免这里定义的模拟,谷歌搜索“fowler mockist classicist”。你会发现很多意见。
正如投票最多的答案所提到的,Martin Fowler 在 Mocks Aren't Stubs 中讨论了这些区别,特别是副标题 The Difference Between Mocks and Stubs,因此请务必阅读该文章。
与其关注这些事物的不同之处,我认为关注为什么这些是不同的概念会更有启发性。每一个都是为了不同的目的而存在的。
假货
fake 是一种行为“自然”但不是“真实”的实现。这些都是模糊的概念,所以不同的人对什么是假的有不同的理解。
伪造的一个例子是内存数据库(例如使用带有 :memory:
存储的 sqlite)。你永远不会将它用于生产(因为数据没有持久化),但它完全可以作为在测试环境中使用的数据库。它也比“真正的”数据库轻得多。
再举一个例子,也许您在生产中使用某种对象存储(例如 Amazon S3),但在测试中您可以简单地将对象保存到磁盘上的文件中;那么您的“保存到磁盘”实现将是假的。 (或者你甚至可以通过使用内存文件系统来伪造“保存到磁盘”操作。)
作为第三个示例,想象一个提供缓存 API 的对象;实现正确接口但根本不执行缓存但总是返回缓存未命中的对象将是一种假的。
fake 的目的不是为了影响被测系统的行为,而是为了简化测试的实现(通过去除不必要的或重量级的依赖)。
存根
存根是一种行为“不自然”的实现。它被预先配置(通常由测试设置)以响应具有特定输出的特定输入。
存根的目的是让您的被测系统进入特定状态。例如,如果您正在为与 REST API 交互的某些代码编写测试,您可以使用始终返回预设响应或以特定错误响应 API 请求的 API 来存根 REST API。这样你就可以编写测试来断言系统如何对这些状态做出反应;例如,测试您的用户在 API 返回 404 错误时得到的响应。
存根通常被实现为仅响应您告诉它响应的确切交互。但是,使某个东西成为存根的关键特性是它的目的:存根就是设置您的测试用例。
模拟
模拟类似于存根,但添加了验证。模拟的目的是断言您的被测系统如何与依赖项交互。
例如,如果您正在为将文件上传到网站的系统编写测试,您可以构建一个接受文件的模拟,并且您可以使用它来断言上传的文件是正确的。或者,在较小的规模上,通常使用对象的模拟来验证被测系统是否调用了模拟对象的特定方法。
模拟与交互测试相关,这是一种特定的测试方法。喜欢测试系统状态而不是系统交互的人会谨慎使用模拟。
测试双打
假货、存根和模拟都属于测试替身的范畴。测试替身是您在测试中使用的任何对象或系统,而不是其他东西。大多数自动化软件测试都涉及使用某种类型的测试替身。其他类型的测试替身包括虚拟值、间谍和 I/O 黑洞。
您在其上声明的东西称为模拟对象。
刚刚帮助测试运行的所有其他内容都是存根。
Unit testing
- 是一种单元(类、方法)受控制的测试方法。
Test double
- 不是主要对象(来自 OOP 世界)。它是一种在临时测试、检查或开发过程中创建的实现。并且它们是为测试单元(方法、类...)的关闭依赖项而创建的
测试双打类型:
假对象是接口(协议)的真实实现或使用继承或其他可用于创建的方法的扩展 - 是依赖关系。通常它是由开发人员创建的,作为替代某些依赖项的最简单解决方案
存根对象是一个裸对象(0、nil 和没有逻辑的方法),具有额外的状态,由开发人员预定义以定义返回值。通常它是由框架创建的
class StubA: A {
override func foo() -> String {
return "My Stub"
}
}
模拟对象与存根对象非常相似,但是在程序执行期间会更改额外的状态以检查是否发生了某些事情(调用了方法、参数、何时、多久...)。
class MockA: A {
var isFooCalled = false
override func foo() -> String {
isFooCalled = true
return "My Mock"
}
}
间谍对象是具有“部分模拟”的真实对象。这意味着您使用非双重对象,除了模拟行为
虚拟对象是运行测试所必需的对象,但该对象的任何一个变量或方法都不会被调用。
存根与模拟
不同之处在于 stub 使用状态验证,而 mock 使用行为验证。
为了说明存根和模拟的用法,我还想包含一个基于 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 服务使用存根来处理前两个问题。为了解决第三个问题,我们可以为电子邮件服务使用模拟对象。
假货是一个通用术语,可用于描述存根或模拟。在我们的测试中,我们将有两个假货。一种是电子邮件服务模拟,我们将使用它来验证是否将正确的参数发送到电子邮件服务。另一个将是一个存根,我们将使用它来模拟从 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);
}
}
所有这些都称为测试替身,用于注入测试用例所需的依赖项。
https://i.stack.imgur.com/TYjQc.png
https://i.stack.imgur.com/1hlgy.png
https://i.stack.imgur.com/uOFYY.png
https://i.stack.imgur.com/Gir5q.png
https://i.stack.imgur.com/3KSWJ.jpg
如果您熟悉 Arrange-Act-Assert,那么解释存根和模拟之间可能对您有用的区别的一种方法是存根属于排列部分,因为它们用于排列输入状态,而模拟属于断言部分,因为它们用于断言结果。
傻子什么都不做。它们仅用于填充参数列表,以免出现未定义或空错误。它们的存在也是为了满足静态类型语言中的类型检查器,以便您可以编译和运行。
Stub、Fakes 和 Mocks 在不同的来源中具有不同的含义。我建议你介绍一下你的团队内部术语并同意它们的含义。
我认为区分两种方法很重要: - 行为验证(暗示行为替代) - 最终状态验证(暗示行为模拟)
考虑发送电子邮件以防出错。进行行为验证时 - 您检查 IEmailSender
的方法 Send
是否已执行一次。并且您需要模拟此方法的返回结果,返回已发送消息的 Id。所以你说:“我希望 Send
会被调用。我只会为任何调用返回虚拟(或随机)ID”。这是行为验证:emailSender.Expect(es=>es.Send(anyThing)).Return((subject,body) => "dummyId")
进行状态验证时,您需要创建实现 IEmailSender
的 TestEmailSender
。并实现 Send
方法 - 通过将输入保存到将用于未来状态验证的某些数据结构(例如某些对象数组 SentEmails
),然后测试您将检查 SentEmails
是否包含预期的电子邮件。这是状态验证:Assert.AreEqual(1, emailSender.SentEmails.Count)
从我的阅读中,我了解到行为验证通常称为 Mocks。状态验证通常称为 Stubs 或 Fakes。
这是使测试富有表现力的问题。如果我希望测试描述两个对象之间的关系,我会对 Mock 设定期望。如果我正在设置一个支持对象以使我了解测试中的有趣行为,我会存根返回值。
stub 和 fake 是对象,因为它们可以根据输入参数改变它们的响应。它们之间的主要区别在于,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;
}
}
根据 Vladimir Khorikov 的《单元测试原则、实践和模式》一书:
模拟:帮助模拟和检查结果交互。这些交互是 SUT 对其依赖项进行的调用以更改其状态。换句话说,它有助于检查 SUT 的交互(行为)及其依赖关系。模拟可能是:间谍:手动创建模拟:使用框架创建
间谍:手动创建
模拟:使用框架创建
存根:有助于模拟传入的交互。这些交互是 SUT 对其依赖项进行的调用以获取输入数据。换句话说,它有助于测试传递给 SUT 的数据。它可能是 3 种类型 Fake:通常用于替换尚不存在的依赖项。 Dummy:是硬编码的值。存根:您配置为针对不同场景返回不同值的成熟依赖项。
Fake: 通常用于替换尚不存在的依赖项。
Dummy:是硬编码的值。
存根:您配置为针对不同场景返回不同值的成熟依赖项。
不定期副业成功案例分享