一文让你掌握单元测试的Mock、Stub和Fake

2023-11-05 23:32

本文主要是介绍一文让你掌握单元测试的Mock、Stub和Fake,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

单元测试中有几个神秘的概念,它们就是Mock,模拟对象;Stub,存根;Fake,伪对象,它们听起来很类似,也很容易混淆,让我们通过这篇文章揭开它们神秘的面纱,探索其幽深的小径。

1.什么是伪对象(Fake)

伪对象,通俗的将就是假货


是用来代替具有“智能”对象的假货实现。通常是一个快捷实现,使它在不同的单元测试中有用,但不能用作集成测试。

到目前为止,我看到的最常见的例子是数据仓储层中。假设我有一个标准的 SQL Server 仓储库,如下所示:

public interface IUserRepository
{void Insert(object user);List<object> GetAllUsers();
}public class UserRepository : IUserRepository
{public List<object> GetAllUsers(){//到数据库取用户集. }public void Insert(object user){//插入用户到数据库}
}

涉及到实际的实现部分时,可能包含逻辑和调用数据库的方法。

当涉及到对可能使用 IUserRepository 的类(例如 UserService)进行单元测试时,我们会遇到一些问题。因为我们不希望我们的单元测试接触到数据库,坦率地说,我们并不真正关心 UserRepository 的实现。

所以我们创建了一个伪对象,而不是直接使用已经实现的真实对象:

public class FakeUserRepository : IUserRepository
{private List<object> _users = new List<object>();public List<object> GetAllUsers(){return _users;}public void Insert(object user){_users.Add(user);}
}

在我们的 fake 中,我们实际上获取了插入的用户,并将其添加到内部列表中。当调用 GetAllUsers 时,我们返回相同的列表。现在,每当单元测试需要调用 IUserRepository 时,我们可以在 FakeUserRepository中进行补充,并且立即“工作”。

这里的主要内容是实现了业务上的相似!

这是一个“真正的”实现,实际上就像一个存储库一样,只是在幕后没有实际的数据库。

2.什么是存根(Stub)

存根是一种返回硬编码响应的实现.

存根没有任何“智能”。没有将对象上的调用捆绑在一起,而是每个方法只返回一个预定义的固定响应。

让我们看看如何为上述创建存根:

public class StubOneUserRepository : IUserRepository
{public List<object> GetAllUsers(){return new List<object>();}public void Insert(object user){//啥都不做~}
}

看起来它有点类似于我们的伪对象,但……不完全是。

这里插入不影响 GetAllUsers,GetAllUsers 本身返回一个没有任何内容的预设响应。我在测试期间对这个对象所做的任何事情都不会改变它的功能。

存根用于满足代码内部的条件,而不是测试功能。

如果我的代码在存储库上调用“插入”,但我并不真正关心我的特定测试的数据会发生什么,那么存根是有意义的,这就省去了编写伪对象“智能”业务的工作。

仓储库的例子显得有些奇葩,因为仓储库总是应该返回动态数据来测试代码中的各种条件。因此,让我使用另一个在现实世界中更有可能需要存根的示例。

假设有一个界面告诉用户是否经过“身份验证”。它看起来像这样:

public interface IUserAuthenticatedCheck
{bool IsUserAuthenticated();
}

现在对于我们的测试,总是需要对用户进行身份验证,也许是为了满足一些基础框架条件。可以像这样定义存根:

public class StubUserAuthenticatedCheckTrue : IUserAuthenticatedCheck
{//返回验证过~~~public bool IsUserAuthenticated() => true;
}

没有是否应该对用户进行身份验证的智能算法,没有其他值,只是一个直接的“总是返回真”的方法。

固定,就是存根擅长的地方。

3.什么是模拟对象(Mock)

模拟是一个预设的对象,可以将动态响应/行为定义为测试的一部分,并预先定义好。

它们不需要去特别实现或实例化,并且(通常)不需要在测试之间共享行为。

我们将在哪里使用 Mock 呢?是您想要相对动态的任何地方,对于特定测试满足条件。

假设我正在编写一个调用以下接口的测试:

public interface IShopService
{bool CheckShopIsOpen(int shopId);
}

我们所做的就是检查商店是开还是关。这个实际实现类可能会调用数据库或某种 webservice/api,但我们不想将其作为单元测试的一部分。

如果在这里使用Fake伪对象,我们需要添加一些虚拟方法来判断商店是应该开还是关。也许是这样的:

public class FakeShopService : IShopService
{public bool ShouldShopBeOpen { get; set; }public bool CheckShopIsOpen(int shopId){return ShouldShopBeOpen;}
}

呃,好像有些复杂,为了能够控制商店是开放还是关闭,我们需要添加新方法。

如果使用存根Stub,必须将真/假响应硬编码到具体类中。可能是这样的:

public class StubShopService : IShopService
{private Dictionary<int, bool> _shops = new Dictionary<int, bool>{{ 1, true },{ 2, false }};public bool CheckShopIsOpen(int shopId){return _shops[shopId];}
}

这适用于预定义的 id 列表,以及商店是开还是关。

但是如果您在测试中使用它并传入 1 的 id,从测试中并不能立即清楚为什么得到 true 的响应,可能需要回来看看你的硬编码?

那么如何使用模拟对象来解决这个问题?(当然建议你直接使用 Moq 库!):

var _mockShopService = new Mock<IShopService>();
_mockShopService.Setup(x => x.CheckShopIsOpen(1)).Returns(true);

就在测试代码中,当使用模拟对象 ID 为 1 的 CheckShopIsOpen 时,非常清楚,返回 true。

它也是特定于这个测试的,并且不会强迫我们在任何地方硬编码任何东西,或者创建具体的类。

当我们有一个测试要求商店 id 1 为假时…

_mockShopService.Setup(x => x.CheckShopIsOpen(1)).Returns(false);

so easy!

4. 何时使用 Mock、Fake 和 Stub

一切并不是绝对的,你懂得!

  • 当想要一个可重用的具体实现时,请使用 Fake,该实现与真实实现类似,具有跨测试的可重用性(例如内存数据库)

  • 当想要在测试中重复使用的硬编码响应/实现时使用存根Stub

  • 当需要对单个测试进行动态响应时使用 Mock

不过,一般测试可能并不会分的这么严格,大部分情况下,我只使用Mock!不服来战~~~~

好了,希望您能更好地理解这些测试对象的用途,并对何时使用每个对象有更多的了解。

最后,祝你好运!

5.小结

关注我,不迷路!看到这了,点个赞再走!
(思聪语录)别嫌我臭,我就是你的臭宝~~

这篇关于一文让你掌握单元测试的Mock、Stub和Fake的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/353172

相关文章

生信代码入门:从零开始掌握生物信息学编程技能

少走弯路,高效分析;了解生信云,访问 【生信圆桌x生信专用云服务器】 : www.tebteb.cc 介绍 生物信息学是一个高度跨学科的领域,结合了生物学、计算机科学和统计学。随着高通量测序技术的发展,海量的生物数据需要通过编程来进行处理和分析。因此,掌握生信编程技能,成为每一个生物信息学研究者的必备能力。 生信代码入门,旨在帮助初学者从零开始学习生物信息学中的编程基础。通过学习常用

如何掌握面向对象编程的四大特性、Lambda 表达式及 I/O 流:全面指南

这里写目录标题 OOP语言的四大特性lambda输入/输出流(I/O流) OOP语言的四大特性 面向对象编程(OOP)是一种编程范式,它通过使用“对象”来组织代码。OOP 的四大特性是封装、继承、多态和抽象。这些特性帮助程序员更好地管理复杂的代码,使程序更易于理解和维护。 类-》实体的抽象类型 实体(属性,行为) -》 ADT(abstract data type) 属性-》成

springboot+maven搭建的项目,集成单元测试

springboot+maven搭建的项目,集成单元测试 1.在pom.xml文件中引入单元测试的依赖包 <!--单元测试依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></depen

JAVA初级掌握的J2SE知识(二)和Java核心的API

/** 这篇文章送给所有学习java的同学,请大家检验一下自己,不要自满,你们正在学习java的路上,你们要加油,蜕变是个痛苦的过程,忍受过后,才会蜕变! */ Java的核心API是非常庞大的,这给开发者来说带来了很大的方便,经常人有评论,java让程序员变傻。 但是一些内容我认为是必须掌握的,否则不可以熟练运用java,也不会使用就很难办了。 1、java.lang包下的80%以上的类

JAVA初级掌握的J2SE知识(一)

时常看到一些人说掌握了Java,但是让他们用Java做一个实际的项目可能又困难重重,在这里,笔者根据自己的一点理解斗胆提出自己的一些对掌握Java这个说法的标准,当然对于新手,也可以提供一个需要学习哪些内容的参考。另外这个标准仅限于J2SE部分,J2EE部分的内容有时间再另说。 1、语法:必须比较熟悉,在写代码的时候IDE的编辑器对某一行报错应该能够根据报错信息知道是什么样的语法错误并且知道

作为刚从事Java开发的小白,需要掌握哪些技能

作为一个刚踏入Java开发世界的小白,面对各种技术和工具,你可能会觉得有点不知所措。但是别担心,我会给你一个简单清晰的路线图,让你可以有条不紊地掌握基本技能,逐步成长为一名Java开发者。 1. 扎实的Java基础 Java的基础是你迈向高级开发的重要基石,建议从以下几个方面着手: 语法和基础概念:比如变量、条件语句、循环、方法、数组、面向对象编程(OOP)等等。这些基础如同建房子的地基,越

掌握Hive函数[2]:从基础到高级应用

目录 高级聚合函数 多进一出 1. 普通聚合 count/sum... 2. collect_list 收集并形成list集合,结果不去重 3. collect_set 收集并形成set集合,结果去重  案例演示 1. 每个月的入职人数以及姓名  炸裂函数  概述  案例演示 1. 数据准备 1)表结构 2)建表语句 3)装载语句 2. 需求 1)需求说明 2)答

Post-Training有多重要?一文带你了解全部细节

1. 简介 随着LLM学界和工业界日新月异的发展,不仅预训练所用的算力和数据正在疯狂内卷,后训练(post-training)的对齐和微调方法也在不断更新。InstructGPT、WebGPT等较早发布的模型使用标准RLHF方法,其中的数据管理风格和规模似乎已经过时。近来,Meta、谷歌和英伟达等AI巨头纷纷发布开源模型,附带发布详尽的论文或报告,包括Llama 3.1、Nemotron 340

PowerMock 单元测试总结与常见坑解决方案

PowerMock 单元测试总结与常见坑解决方案 官方文档: PowerMock GitHub PowerMock 在单元测试中能够帮助我们解决静态类、final 方法、私有方法等无法轻易 mock 的问题。下面是我在实际使用 PowerMock 时踩过的一些坑,并结合 PowerMock 的一些方法进行总结。 基本依赖和设置 在 Maven 中添加 PowerMock 依赖。在测试

掌握Go语言中的时间与日期操作

Go语言中的时间与日期操作 在编写程序时,处理时间和日期看似是一项无关紧要的任务,但在需要同步多个任务或从文本文件中读取时间时,它的重要性便凸显出来。Go语言中的time包为我们提供了丰富的时间与日期操作功能。本文将详细介绍如何在Go语言中解析时间与日期字符串、在不同的格式之间进行转换,以及如何按照所需的格式输出时间和日期。 初识time包 在学习如何解析字符串并将其转换为时间或日期之前,我