单元测试探析:什么是Stubs、Mocks、Spies、Dummies?带你了解4个核心工具

article/2025/9/24 15:38:50

在单元测试中,对象之间的依赖往往交织到一起,需要拆成各个单元才能逐个击破,这也是单元测试的目的。如何将这些交织到一起的对象拆开,需要一些工具,这些工具业内人们称其为“测试替身”。

本文作者介绍了单元测试中的4个“测试替身”工具,即Stubs、Mocks,、Spies 和 Dummies。

Stubs 为被测试对象提供数据,没有任何行为,往往是测试对象依赖关系的上游。

Spies 被依赖对象的代理,行为往往由被代理的真实对象提供,代理的目的是为了断言程序运行的正确性。

Mocks 模拟一个具有特性行为的对象,在测试开始前根据期望提供需要的结果。被测试对象往往调用这个对象的方法时,根据条件得到不同的输入,从而满足测试对象的不同场景。例如,mock 数据库的存储层,返回正常数据、空或者丢出异常等情况。

Dummy 被用来仅仅作为填充参数列表的对象,实际上不会用到它们,对测试结果也没有任何影响。

以下为作者观点。

你可能讨厌或喜欢单元测试,这取决于你,但事实是,如果你不理解它们背后的概念,你写测试的效率可能就会弄得一团糟。

要成为写单元测试的高手,第一个核心步骤是了解其重点。单元测试不是集成测试,它们必须测试单一的代码单元。

让我们来看看在写单元测试时要用到的4个工具。我指的不是IDE或任何插件或扩展,我指的是概念性的工具:stubs、mocks、spies、 dummies。

什么是Stubs?

我经常看到开发人员通过启动一个“测试数据库”来编写与数据库交互的代码的测试,其中测试可以触发“写入”并通过查询数据库进行验证,我认为这是错误的。

Stubs可以帮助你处理这些情况,即你的代码与第三方服务进行交互。无论是数据库、API还是硬盘上的文件,stubs都提供了使用更简单版本的服务的代码。

这个Stub会返回一个已知的、可控的值。例如,如果你正在测试一个向数据库写值的函数,你应该编写一个Stub,避免与数据库的交互,但返回一个成功的结果。

通过这个,你就可以测试当写入操作工作时发生了什么。然后你可以编写另一个Stub(在另一个测试中),返回一个失败的结果,这样你就可以测试你的逻辑中发生处理错误的部分。

你可以在一个特定的对象中Stub一个函数或一个方法(只要语言允许)。

因此,让我们快速看一个例子:

/// the function to test
function saveUser(usrData, dbConn) {let q = createQueryFromUser(usrData)let result = dbConn.query(q)return result;
}//the stub
makeStub(dbConn, 'query', () => {return true;
})//the test
it("should return TRUE when the query succeeds", () => {let result = saveUser({name: "Fernando",password: "1234"}, dbConn)result.should.be.true
})

上面的例子有几个地方需要解读,同时注意到,虽然这个例子是用伪JavaScript写的,但其概念可以推导到所有语言。

首先是要测试的函数,现在它是一个接收数据的简单函数,一个数据库连接对象,并依靠一个伪createQueryFromUser函数来创建实际的SQL查询。来自dbConn对象的query方法是与数据库交互的方法,也是我们有兴趣Stub的方法,因为我们不希望query真正启动。

这里是Stub发挥作用的地方,makeStub函数负责用我们传递的匿名函数(这是一个伪函数,每次只返回TRUE)神奇地覆盖数据库连接的方法query。

最后,实际的单元测试是利用Stub(因为它之前就被定义了)。这个测试确保我们的函数在进展顺利时返回正确的布尔值(boolean value)。

上面只是一个例子,告诉你可以从Stubs中受益。说实话,在任何时候,如果你有一个具有动态结果的函数,你就必须找到一种方法来确保每次执行测试时都有相同的结果。所以,Stubs可以帮到你。

什么是Mocks?

Mocks就像Stubs的孪生兄弟,它们看起来很像,人们经常把它们混淆,其实它们两个完全不同。

当Stubs允许你替换或重新定义一个函数或方法时,Mocks允许你在真实的对象/函数上设置预期行为。因此,从技术上讲,你并没有替换对象或函数,你只是告诉它在某些非常特殊的情况下该做什么,除此之外,对象仍然照常工作。

让我们看一个例子来理解这个定义:想象一下,要测试一个过道补货功能。它从库存中提取物品,并把它们放在正确的过道上。这里测试的关键是,每次我们补充一个过道时,也需要从库存中取出相同数量的元素。

var inventory = createMock(Inventory("groceries"))
//set expectations
inventory.expect("getItems", 10).returns(TRUE).expect("removeFromInventory", 10).returns(TRUE)var aisle = Aisle("groceries")
aisle.replenish(10, inventory) //executes the normal flow
assertion(aisle.isFull(), "equals to", TRUE)

请记住,在某些情况下,mocks的预期行为会被你所使用的框架自动检查。这就是为什么没有真正的断言来处理期望值的原因,如果它们没有被满足,模拟就会抛出一个异常,测试就不会通过。

在这个特殊的例子中,预期getItems方法将被调用,其属性为10,它将返回TRUE,它也将调用removeFromInventory函数,其属性也是10。最后返回的结果是TRUE。

当然,我们可以用Stubs来完成这个任务,但这不是重点,在许多情况下,这些工具可以用于相同或类似的用例。

Spies到底是什么?

顾名思义,Spies可以让我们了解被测试代码内部发生了什么,即使我们并没有真正访问到它。我知道,这听起来很诡异,但它有它的用途。

换句话说,Spies是收集执行信息的Stubs,因此他们最终可以告诉你调用了什么、何时调用了哪些参数。

想想上面mocks的例子,我们必须事先设定期望值(预期),以确保我们想要的东西都会被执行。我们可以通过 "监视 "库存来检查同样的事情,并询问这些方法是否真的被调用了,用了哪些参数。

我们来看看另一个例子,一个文件读取器函数,一旦它完成了文件处理,也应该关闭文件处理程序。

const filename = "yourfile.txt"
let myspy = new Spy(IOModule, "closeFile") //create a spy for the method closeFile in the module dedicated to I/function readConfigFile(fname) {const reader = new FileReader(filename, IOModule)let content = reader.read()loadConfig(content)IOModule.closeFile(reader);
}//The testit("should call the 'closeFile' method after reading the content of the file", () => {readConfigFile(filename)assertion(myspy.called, "equals to", TRUE)  
})

要测试的函数叫做readConfigFile,它的目的是读取一个文件,并通过调用loadConfig方法将其内容加载为配置。作为测试的一部分,我们有兴趣了解该函数是否真的关闭了文件处理程序。

请记住,这个测试与我上面所说的相反,因为它实际上是在打开和读取文件,这是我们单元测试不应该有的第三方依赖。为了使这个测试完全 “合规”,当我们有兴趣测试成功的读取和失败的读取时,我们还必须为I0Module和控件添加一个stub。

注意:与stubs不同的是,Spies包装目标方法/函数,而不是替换它,因此目标的原始代码也将被执行。

什么是 dummies?

最后,我想介绍的最后一个工具是众多周知的无用的 “dummies”。顾名思义,除了在需要的时候出现之外,没有其他真正的用途。它们的目的是在语法需要时出现在那里。

例如,想象一下必须调用一个需要3个参数的函数,其中第一个参数是另一个函数(外部依赖)。考虑到该函数当前的stub,你知道其他两个属性不会被使用,然而,解释器/编译器正在抱怨你缺少该函数的最后两个属性,所以你需要添加它们。

你怎么能做到这一点呢?

你猜对了,通过dummies。你只需添加2个什么都不做但被编译器接受的dummy对象。

Dummies在强类型语言中使用时更有意义,因为这些类型的检查在那里更常见。例如,看看下面这个TypeScript的例子:

type UserData = {name: string;password: string
}//The function to be tested
function saveUser(usrData: UserData, dbConn: DataBase, validators:DataValidators) {if(!validators.validateUserData(usrData)) {return false;}let query = createQueryFromData(usrData);let result = dbConn.query(query);return result;
}// The test itself//the stub
const stubbedValidators: DataValidators = {validateUserData: (data: UserData) => false;
}//the dummies
const userData: UserData = {name: "", password: ""}
const dbConn: DataBase = {}//the test
it("should return false if the user data is not valid", () => {let result = saveUser(userData, dbConn, stubbedValidators);result.should.be.false;
})

该代码定义了一个新的saveUser函数,该函数也需要一个validators依赖。我们还添加了一个验证步骤,以确保我们试图保存的数据是 “有效的”(不管这意味着什么)。

但我们测试的目的是确保如果数据无效,我们将返回false。这意味着我们没有真正执行任何验证,事实上,我们需要stub那个验证器来控制结果,否则如果明天我们的验证例程发生变化,我们现在可能会传递一个有效的数据样本,测试就会失败。

现在的问题是,通过查看我们的业务逻辑,如果数据是无效的,我们并没有真正使用数据库连接,也没有实际的用户数据。我们需要它们在那里,但我们并不真正需要它们。所以他们实际上已经变成了dummies。

这就是为什么我只是传递假的空对象(A.K.A dummies)作为函数的前两个属性。

Stubs, Mocks, Spies和Dummies是你在测试中所做的一切的面包和黄油,你越是使用它们,就越是感觉熟悉,你就越容易理解如何处理一个新的测试。

例子是否足够清楚?你对这些例子还有疑问吗?请留下评论,我们一起讨论!


资源分享

下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】

在这里插入图片描述

在这里插入图片描述


http://chatgpt.dhexx.cn/article/S9cvG7of.shtml

相关文章

分布式CAP 定理

历史 这个定理起源于柏克莱加州大学University of California, Berkeley的计算机科学家埃里克布鲁尔在2000年的分布式计算原则研讨会(Symposium on Principles of Distributed Computing(PODC))上提出的一个猜想。 在2002年&…

简单解释CAP定理

简单解释下分布式系统中的CAP定理: CAP定理: P-分区容错性, C-一致性, A-可用性 (易 ---> 难) CAP如何选择 > 在不同的业务领域中对CAP的选择有所不同; (典型的情况以及选取的规则) CAP的重要性 > CAP是分布式系统设计中最基础也最为关键的理论! (分布式系统不可能同…

CAP定理以及BASE定理详解

CAP 是分布式系统设计理论,BASE 是 CAP 理论中 AP 方案的延伸。 CAP定理 C:Consistency(一致性)、A: Availability(可用性)、P: Partition Tolerance(分区容错性) 一致性&#xf…

架构设计之「 CAP 定理 」

在计算机领域,如果是初入行就算了,如果是多年的老码农还不懂 CAP 定理,那就真的说不过去了。CAP可是每一名技术架构师都必须掌握的基础原则啊。 现在只要是稍微大一点的互联网项目都是采用 分布式 结构了,一个系统可能有多个节点组…

图解 CAP 定理(转载)

文章目录 CAP 定理不一致的产生解决方案保 CP 失 A保 AP 失 C CAP 定理 Consistency 一致性:访问分布式系统中任意节点,总能返回一致的结果 Every read receives the most recent write or an errorAvailability 可用性:分布式系统总能向客户…

分布式系统的CAP定理

目前只要是大型互联网项目都是采用分布式结构,一个系统可能有多个节点组成,每个节点都可能需要维护一份数据。那么如何维护各个节点之间的状态,如何保障各个节点之间数据的同步问题就是大家急需关注的事情了。CAP定理是分布式系统中最基础的原…

CAP 定理的含义

看到一篇很好的关于 CAP 定理的博文,顺便转发一下,by:阮一峰 分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。 分布式系统的最大难点,就是各个节点的状态如何同步。…

CAP定理和BASE理论

2000 年的时候,Eric Brewer 教授提出了 CAP 猜想,2年后,被 Seth Gilbert 和 Nancy Lynch 从理论上证明了猜想的可能性,从此,CAP 理论正式在学术上成为了分布式计算领域的公认定理。并深深的影响了分布式计算的发展。 …

分布式-CAP定理

在分布式学习中,我们经常遇到一个概念便是CAP,它是分布式很重要的理论基础。很多分布式算法也是在不断的在解决相关问题,今天就让我们重新学习或者回顾一下这个知识点吧,加深印象打牢基础。 一、简介 CAP定理(CAP theo…

佳文分享:CAP定理

1976年6月4号,周5,在远离音乐会大厅的一个楼上的房间内,在位于Manchester的Lesser Free Trade Hall ,Sex Pistols 乐队(注:Sex Pistols的经理人Malcolm McLaren 2010.4.8去世)开始了他们的第一次…

谈谈对CAP定理的理解

谈谈对CAP定理的理解 CAP定理的常规解释是任何分布式系统只能在一致性(Consitency),可用性(Availability)和分区容忍性(Partition Tolerance)中三选二。这个解释很让人费解,笔者在看了一些文章后谈谈我对它的理解,还请斧正。 从问题出发 假设…

分布式系统的 CAP 定理

CAP定理指出,在一个分布式系统中,对于一致性、可用性、分区容错这三个特性,不可能同时满足,而是必须有所舍弃。我们设计分布式系统时,必须在三者之间(尤其是一致性和可用性之间)有所取舍和平衡。…

简述CAP定理

CAP定理示意图: 一.CAP理论概述: CAP定理告诉我们,一个分布式系统不可能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求&…

CAP定理的含义

目录 定理解读 如何抉择 1998年,加州大学的计算机科学家 Eric Brewer 提出了分布式系统的三个指标: C:Consistency,一致性。在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读…

CAP定理是什么?

写在前面 本文隶属于专栏《100个问题搞定大数据理论体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢! 本专栏目录结构和文献引用请见100个问题搞定大数据理论体系 解答 分布式系统不…

分布式CAP定理,为什么不能同时满足三个特性?

在弄清楚这个问题之前,我们先了解一下什么是分布式的CAP定理。 根据百度百科的定义,CAP定理又称CAP原则,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、P…

AMBA总线理解-AXI总线

AXI的设计目标是可以在高始终频率下运行,并且在迟滞时间长的情况下也可以达到高数据吞吐率。他可以将读/写请求和读/写结果相互分离,将数据写入和数据读出的信号分类,并且可以同时进行写入和读出的动作,因此可以大幅度提升数据吞吐…

Zynq AXI总线

S02_CH12_ AXI_Lite 总线详解 - 米联客 - 博客园 12.1前言 ZYNQ拥有ARMFPGA这个神奇的架构,那么ARM和FPGA究竟是如何进行通信的呢?本章通过剖析AXI总线源码,来一探其中的秘密。 12.2 AXI总线与ZYNQ的关系 AXI(Advanced eXtens…

AXI总线协议介绍

AXI总线协议介绍 AXI(Advanced eXtensible Interface)协议是一种面向高性能、高带宽系统设计的总线协议,能够满足各种高速系统的总线协议,能够满足各种高速系统的总线互连。 AXI协议的主要特点有: 独立的地址,控制和数据接口支持使用字节选通的不对齐数据的传输基于特定地…

AXI总线入门

介绍【只要涉及到芯片内部交换都是涉及到AXI协议】 ZYNQFPGAARM 两者间的数据传输 AXI(Advanced extensible Interface)高级外设总线协议,协议是一种通用总线协议,AXI协议是ARM AMBA(Advanced Microcontroller Bus Arc…