使用示例解释.NET中的Mocking是什么?

让我们踏上探索.NET软件开发中Mocking概念的旅程,让我们深入了解Mocking是多么简单易懂、易于访问。请与我一起穿越这个主题,我将涵盖以下内容:

  • 理解Mocking:为何它对于构建强大的测试策略至关重要。
  • 探索一些最常见的Mocking库:如Moq、NSubstitute、FakeItEasy和Rhino Mocks等。
  • 掌握Mocking技术:使用每个库,为您提供选择最佳Mocking工具的知识,以满足您的需求。

本教程的目的是让您具备足够的知识,以便自行决定您偏好的Mocking库,这样您就可以继续在应用程序中编写一些强大的测试。

(本文视频讲解:java567.com)

本教程的先决条件

  1. 理解C#编程
  2. 理解C#单元测试
  3. 一个IDE(Rider、Visual Studio等)或代码编辑器(VS Code、Sublime Text、Atom等)

入门/设置

为了加快进程,我已经为本教程提供了一个公共GitHub仓库。您可以将其克隆到本地计算机上以便跟随操作。

只需转到此链接并将仓库克隆到本地计算机即可。

如果您忘记了如何操作,请快速复习一下:转到上面的链接,在右上角点击“Code”,然后复制提供的URL。

image-2

image-3

找到您的本地git文件夹(如果没有,请在您的用户根文件夹中创建一个)。然后在您喜欢的终端中导航到您的git文件夹,并执行以下命令。

//(用URL替换<url>)
git clone <url> 

应用程序简要概述

您刚刚克隆的解决方案是一个基本的Web API项目,它引用了一个包含一些Todo领域对象和服务的类库,这些对象和服务将改变一个todo项目列表。

为了本教程的目的,这些元素存储在内存列表中,而不是连接到数据库。但您可以使用数据库或其他形式的数据持久化方法。

然而,对于本教程的目的,我们不太关心数据的持久化,而更关心的是对这个服务进行Mocking。

Mocking是什么?

带有文字“Real”和“Fake”的两个箭头通过Mocking,你无法区分真实与虚假

Mocking在软件开发中是模拟系统测试中某个部分依赖的类、方法或服务的行为或返回对象的概念。

在测试特定组件或代码单元时,通常需要将其与其依赖项(如数据库、Web服务或其他类)隔离开来,以确保测试仅专注于被测试单元的功能,而不必关注代码的外部方面。

Mocking允许开发人员创建这些依赖项的模拟对象或虚拟实现,以预定的方式行为,模仿真实对象的行为。

通过使用模拟对象或方法,我们可以控制依赖项的输入和输出。这使得测试不同的情景变得更加容易,其中业务逻辑依赖于依赖项的结果。

示例

假设我们有一个API端点调用一个连接到数据库的存储库。我们的API响应类型取决于存储库返回的值:我们从API返回一个400或200的响应。

我们可以简单地模拟存储库返回值,并测试我们的API在这两种情况下是否返回了正确的响应,而无需实际触及数据库。

实质上,Mocking通过隔离待测试的代码并为其依赖项提供可预测的行为,帮助开发人员编写更可靠、高效的测试,从而提高软件的整体质量。

Mocking的好处是什么?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传好处

代码隔离

正如我已经解释过的那样,Mocking依赖项允许对代码进行测试隔离。通过Mocking依赖项,您可以断言被测试代码中的失败点,而不是依赖项本身(除非您Mocking它们错误——希望本教程能帮助您避免这种情况)。

更快的测试速度

通过用Mocking实现替换真实依赖项,可以更快地进行测试。您不必与这些依赖项的不一致性、不可靠或不可预测的结果作斗争。这消除了设置和拆卸外部资源的需要,这有时可能会很复杂且耗时。

确定性测试

Mock对象提供了一个可控制的环境,使开发人员能够指定依赖项的确切行为和响应。这种方法意味着测试是一致的,从而更容易找到错误并调试问题,特别是采用TDD(测试驱动开发)方法的情况下。

并行测试

Mocking通过消除对可能在测试期间受限或无法访问的外部资源的依赖,实现了并行测试(同时运行多个测试)。

例如,如果您没有Mocking您的数据库连接层,尝试并行运行测试可能会导致不一致的通过/失败度量,因为另一个测试可能会影响您在另一个测试中使用的数据库表。Mocking这一层意味着您的测试现在与此层无关,可以同时运行。

减少测试维护工作

由于Mock对象封装了依赖项的行为,因此对这些依赖项的更改不一定需要更新测试本身。这减少了与不断发展的代码库和依赖项相关的维护开销。

增强的测试覆盖范围

Mocking允许开发人员模拟各种场景和边界情况。通过控制依赖项的行为,开发人员可以确保他们的测试覆盖了代码的所有相关路径,消除了任何现实生活或物理限制。

使用Mocking需要注意的事项

image-20

复杂性: 有时在对应用程序的复杂区域进行Mocking/测试时,Mocking也可能变得复杂。然而,如果系统过于难以Mocking,您可能需要重新评估您的架构。

学习曲线: 这需要理解Mocking库的语法和概念,这对于刚接触单元测试或特定框架的开发人员来说可能是具有挑战性的。

过度规范化: Mocking可能导致对被测试代码行为进行过度规范化。这意味着测试可能会与实现细节紧密耦合,使其变得脆弱,并在实现更改时容易中断。在验证行为和专注于期望结果之间保持平衡至关重要。

要注意错误的正测试: 虽然Mocking允许您隔离代码单元,但它也可能会产生一种虚假的安全感。Mock模拟依赖项,但它们可能无法完全复制真实依赖项的行为。仍然需要进行集成测试或端到端测试来验证系统的整体行为。

流行的.NET Mocking库

image-25

以下是一些流行的.NET测试库:

  • FakeItEasy
  • NSubstitute
  • Moq
  • Rhino Mocks

这只是在线可用的一些最常用的.NET Mocking库的列表,但还有许多其他库。我强烈建议使用其中之一,因为它们有着更大的社区、更值得信赖的代码库和良好的文档(在开始时至关重要)。

这些Mocking库都有自己创建对象的语法,但它们都遵循相同的原则。

  1. 声明要Mock的对象/类型/服务
  2. 您希望该对象/类型/服务如何运行(实现)
  3. 返回值是什么(在必要时)。

查看测试

如果您打开解决方案,并导航到“Test”项目,您会发现我们在那里有四个文件,每个文件中都包含不同的Mocking库测试。

  1. FakeItEasyApiTests.cs
  2. MoqApiTests.cs
  3. NSubstituteApiTests.cs
  4. RhinoMocksApiTests.cs

在这些文件中,您将看到我们有四个非常基本的XUnit测试。出于本教程的目的,我将它们保持简短和简单。

深入了解测试结构

每个测试文件都使用构造函数来初始化其各自服务的私有版本,您可以看到这些服务在不同库之间的差异,但仍遵循相同的概念。

// FakeItEasy
 _fakeTodoService = A.Fake<ITodoService>();

// NSubstitute
  _substituteTodoService = Substitute.For<ITodoService>();
  
// Moq
 _mockTodoService = new Mock<ITodoService>();

// Rhino Mocks
 _mockTodoService = MockRepository.GenerateMock<ITodoService>();

选择“正确”的Mocking库完全取决于个人偏好,以及您认为哪种更容易编写、使用和阅读/理解。

有些人可能会发现使用类似于FakeSubstitute.For这样的词更容易理解或阅读。而其他人可能会觉得A.Fake不直观,更喜欢new Mock<type>更明显。

排列、执行和断言

遵循AAA(排列、执行和断言)测试原则,我们可以仔细构建我们的测试,使得我们正在做什么和在哪里做什么变得明显。

AAA测试方法包括三个步骤:

  1. 排列:设置测试环境,模拟的服务/外部依赖项。
  2. 执行:执行正在测试的操作。
  3. 断言:验证期望的结果。

使用模拟对象模拟返回项

让我们通过模拟TodoService.GetAllTodos方法返回一组存根任务来测试getAll(GetAllTodoItems)端点。

这种方法消除了为每个测试场景设置和填充数据库的需要,确保针对特定标准测试返回值的API端点。

模拟提供了一个理想的解决方案,允许我们模拟所需的行为,而不受其他测试的干扰。

我们可以这样做(记住完整的代码在存储库中可用):

// FakeItEasy
A.CallTo(() => _fakeTodoService.GetAllTodos()).Returns(expectedTodos);

// NSubstitute
 _substituteTodoService.GetAllTodos().Returns(expectedTodos);
 
 // Moq
 _moqTodoService.Setup(s => s.GetAllTodos()).Returns(expectedTodos);
 
 // Rhino Mocks
 _mockTodoService.Stub(s => s.GetAllTodos()).Return(expectedTodos);

这些方法是做什么的?

您将在大多数库中看到的一个常见特性是使用lambda函数来指示要模拟的方法。

在设置方法中提供的lambda函数实际上是一个配置步骤,定义了调用模拟方法时应采取的操作。此配置在测试期间调用模拟方法时存储和应用。

让我们分解一下,我们正在做什么:

  1. Lambda指定了我们希望在模拟服务上配置/设置的方法。
  2. 我们传递的lambda不会立即由设置方法运行。我们没有要求测试立即运行该方法;我们的意思是,“记住这个指令/设置,以备在测试期间调用实际方法时使用。”
  3. 当我们模拟的方法在测试期间被调用时,它会检查调用签名是否与我们提供的设置配置相匹配。 如果匹配,测试会遵循设置期间给出的指示。

重要说明:

另一方面,NSubstitute允许开发人员直接在虚拟对象上模拟方法。这意味着您可以访问虚拟的GetAllTodos方法,并简单地将返回值设置为您期望的值。

虽然Moq使用了Setup方法,但与其他方法略有不同。Moq在内部创建一个代理对象,代表了被模拟的对象,并公开了.Object属性来访问它。我们将在下一部分中看到这是如何工作的。

如何调用被测试系统(SUT)

// FakeItEasy
var sut = new TodoController(_fakeTodoService);

// NSubstitute
var sut = new TodoController(_substituteTodoService);

// Moq -- 和其他的略有不同
 var sut = new TodoController(_moqTodoService.Object);

// Rhino Mocks
var sut = new TodoController(_mockTodoService);

在四个库中的三个中,您可以直接传递模拟对象。然而,Moq需要在模拟上使用.Object属性才能使用它。因此,我们将moqTodoService.Object作为控制器的参数传递。

image-26一张图片显示所有测试都通过了

运行测试,您可以看到它们都通过了。如果您更改存储库中的任何代码,这不会有任何影响,因为这些测试是模拟的。试一试,尝试更改存储库功能并重新运行测试,它们将继续通过。

我们专注于端点的功能,而不是模拟存储库正在执行的操作,这就是模拟的美妙之处。

Mocking的选择是无穷无尽的

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

模拟不仅允许我们设置模拟对象返回的内容,还可以允许我们模拟实现,包括抛出特定错误,以便测试我们的API错误处理和不正常路径。

这在每个库测试文件中的Delete_Returns500_AndErrorMessageThrown_WhenExceptionThrown测试中有所体现。

// FakeItEasy
A.CallTo(() => _fakeTodoService.Delete(1)).Throws(new Exception(errorMessage));

// NSubstitute
_substituteTodoService.When(x => x.Delete(1)).Do(x => throw new Exception(errorMessage));

// Moq
_moqTodoService.Setup(s => s.Delete(1)).Throws(new Exception(errorMessage));

// Rhino Mocks
 _mockTodoService.Stub(s => s.Delete(1)).Throw(new Exception(errorMessage));

使用不同的库,我们可以使服务上的Delete方法抛出我们想要的任何异常。当您想要返回不同的状态代码或根据抛出的异常类型以不同的方式处理错误时,这是理想的。

例如,我们可以将Throws(new Exception(errorMessage)更改为Throws(new UnauthorizedAccessException(),并测试当抛出401状态代码时。

全局设置

您可以为同一方法分配多个配置。这在您想要在一个地方设置模拟对象的所有配置时非常有用。例如,在测试类构造函数中。

在某些测试框架(如NUnit)中,您可以在方法上方使用[OneTimeSetUp]属性,该属性在测试用例之前运行,或者简单地使用测试类构造函数。

在这种情况下,您可以像在Moq中那样做一些事情:

public MoqApiTests()
{
        _mockTodoService = new Mock<ITodoService>();
        _mockTodoService.Setup(x => x.Delete(1)).Throws(new Exception("This is a generic exception"));
        _mockTodoService.Setup(x => x.Delete(2)).Throws(new UnauthorizedAccessException("You cannot perform this action on this item"));
}

在这个例子中,我们演示了设置模拟服务调用相同方法的多个配置,每个配置导致抛出不同的异常。

这种方法有助于在不同的测试中测试不同异常发生时的不同结果,而不会使我们的测试代码因重复的设置而杂乱无章。

例如:

// 测试1
var result = TodoController.Delete(1);
// 断言处理一般异常

// 测试2
var result = TodoController.Delete(2); 
// 断言处理UnauthorizedAccessException

我更喜欢在每个单独的测试中设置模拟,以确保没有外部因素影响模拟。

这样,我可以在测试中轻松识别被模拟的内容,而不必在其他地方搜索模拟对象和设置。

如果我不在乎我传递的是什么?

在我们的删除示例中,我们始终传递了一个ID给模拟实现。因此,如果我们通过TodoController.DeleteTodoItem调用传递了一个不同的ID,比如101,我们将不会收到相同的结果。

这是因为我们明确指示了模拟对象在调用存根方法时使用ID 1时抛出错误。

为了解决这个问题,我们可以更不具体。每个库都有自己的语法,使我们能够指定如果传递给方法的是任何整数,它将抛出特定的异常。

// FakeItEasy
A.CallTo(() => _fakeTodoService.Delete(A<int>._)).Throws( new Exception(errorMessage));

// NSubstitute
_substituteTodoService.When(x => x.Delete(Arg.Any<int>())).Do(x => throw new Exception(errorMessage));

// Moq
_mockTodoService.Setup(s => s.Delete(It.IsAny<int>())).Throws(new Exception(errorMessage));

// Rhino Mocks
_mockTodoService.Stub(s => s.Delete(Arg<int>.Is.Anything)).Throw(new Exception(errorMessage));

此代码表示当传递任何int类型的参数时,它应该抛出此异常。

NSubstitute的语法略有不同:它需要在遇到这种情况时明确指示要抛出指定的错误,不像我们在通知它返回对象时那样。这种差异源于库的内部机制。

断言模拟对象被调用

在某些情况下,您可能希望验证模拟服务是否以正确的参数被调用。当处理“发出并忘记”服务时,这特别有用。

在这种情况下,您的API端点被调用,虽然它总是返回true,但它也会触发一个独立执行的服务执行某些操作,这不会影响API的返回类型(也许是一个异步通知服务)。

这是您可能希望执行一个快速的健全性检查以确保您的“发出并忘记”服务被调用的少数情况之一(尽管理想情况下,您会进行与该服务的集成测试)。

如果您查看DeleteTodoItem端点以及每个测试文件中的DeleteAPI_CallsNotificationService_WithTaskId_AndUserId测试,您可以看到如何完整地执行此操作的示例。

我们正在验证当我们调用DeleteTodoItem时,在我们的正常路径上,NotificationService.NotifyUserTaskCompleted被调用,传递了要删除的项目的ID和硬编码的用户ID。

作为练习,您可以创建一个用户服务,该服务返回已登录用户的ID,并将其用于传递ID给服务。这也可以在测试中进行模拟。

 // FakeItEasy
  A.CallTo(() => _fakeNotificationService.NotifyUserTaskCompleted(1,1)).MustHaveHappened(1, Times.Exactly);
  
// NSubstitute _notificationService.Received().NotifyUserTaskCompleted(1,1);

// Moq 
 _moqNotificationService.Verify(x => x.NotifyUserTaskCompleted(1,1)); // Defaults to Times.AtLeastOnce
 
// Rhino Mock
_mockNotificationService.AssertWasCalled(x=>x.NotifyUserTaskCompleted(1,1));

结论

总之,模拟对象的灵活性提供了许多应用场景,在测试单个代码单元时不可或缺。

尽管我已经涵盖了模拟的几个功能和可实现的验证,但还有更多,比如方法调用顺序和负验证。

在我看来,项目中选择模拟库是主观的,没有明确的正确或错误选项。虽然一些库可能提供更方便的扩展或更清晰的语法,但最终决定归根结底取决于个人偏好。

我希望这个教程为您提供了对模拟世界的一瞥,并阐明了不同库之间的语法差异。

(本文视频讲解:java567.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/557251.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python教学入门:字典和集合

字典&#xff08;Dictionary&#xff09;&#xff1a; 定义&#xff1a; 字典是 Python 中的一种数据结构&#xff0c;用于存储键值对&#xff08;key-value pairs&#xff09;。字典使用花括号 {} 定义&#xff0c;键值对之间用冒号 : 分隔&#xff0c;每对键值对之间用逗号 …

SQL-Oracle 获取最大值,第二大,第三大,第 N 大值

目录 1、原始数据2、获取最大值记录3、获取第二大值记录4、获取第三大值记录 1、原始数据 select * from test_2024_04_15_001 order by 销量 desc,渠道2、获取最大值记录 select 渠道,销量 from ( select a.渠道, a.销量 from test_2024_04_15_001 a order by a.销量 desc,…

AI边坡监测识别摄像机

AI边坡监测识别摄像机是一种利用人工智能技术进行边坡监测的智能设备&#xff0c;其作用是及时监测边坡变化并识别潜在的滑坡、崩塌等危险情况&#xff0c;以提供及时预警和采取必要的安全措施。这种摄像机通过高清摄像头实时捕捉边坡的图像&#xff0c;并利用AI算法对边坡的形…

实验室三大常用仪器2---函数信号发生器的基本使用方法(笔记)

目录 函数信号发生器的基本使用方法 如何连接函数信号发生器和示波器 实验室三大常用仪器1---示波器的基本使用方法&#xff08;笔记&#xff09;-CSDN博客 实验室三大常用仪器3---交流毫伏表的使用方法&#xff08;笔记&#xff09;-CSDN博客 示波器是用来显示和测量信号的…

Java | Leetcode Java题解之第35题搜索插入位置

题目&#xff1a; 题解&#xff1a; class Solution {public int searchInsert(int[] nums, int target) {int n nums.length;int left 0, right n - 1, ans n;while (left < right) {int mid ((right - left) >> 1) left;if (target < nums[mid]) {ans mi…

阿里云图片处理之 图片样式(裁剪)

文档 : https://help.aliyun.com/zh/oss/user-guide/image-styles?spma2c4g.11186623.0.0.5961fe7aq3111v 需求 : 由于客户端界面展示的图片较多, 而且每个图片都过大并且高清高分辨率的, 导致打开页面时图片加载很慢, 而且是缩略图, 对图片清晰度要求不是那么得高, 因此可以…

ASP.NET MVC企业级程序设计 (商品管理:小计,总计,删除,排序)

目录 效果图 实现过程 1创建数据库 2创建项目文件 3创建控制器&#xff0c;右键添加&#xff0c;控制器 ​编辑 注意这里要写Home​编辑 创建成功 数据模型创建过程之前作品有具体过程​编辑 4创建DAL 5创建BLL 6创建视图&#xff0c;右键添加视图 ​编辑 7HomeCont…

【问题处理】银河麒麟操作系统实例分享,adb读写缓慢问题分析

1.问题环境 处理器&#xff1a; HUAWEI Kunpeng 920 5251K 内存&#xff1a; 512 GiB 整机类型/架构&#xff1a; TaiShan 200K (Model 2280K) BIOS版本&#xff1a; Byosoft Corp. 1.81.K 内核版本 4.19.90-23.15.v2101.ky10.aarch64 第三方应用 数据库 2.问题…

Spring Security详细学习第一篇

Spring Security 前言Spring Security入门编辑Spring Security底层原理UserDetailsService接口PasswordEncoder接口 认证登录校验密码加密存储退出登录 前言 本文是作者学习三更老师的Spring Security课程所记录的学习心得和笔记知识&#xff0c;希望能帮助到大家 Spring Sec…

buuctf——[ZJCTF 2019]NiZhuanSiWei

buuctf——[ZJCTF 2019]NiZhuanSiWei 1.绕过file_get_contents()函数 file_get_contents函数介绍 定义和用法 file_get_contents() 把整个文件读入一个字符串中。 该函数是用于把文件的内容读入到一个字符串中的首选方法。如果服务器操作系统支持&#xff0c;还会使用内存映射…

python实现将数据标准化到指定区间[a,b]+正向标准化+负向标准化

目录 一、公式介绍 (一)正向标准化公式 (二)负向标准化公式如下 (三)[a,b]取[0,1]的特例 二、构建数据集 三、自定义标准化函数 四、正向标准化 五、负向标准化 六、合并数据 一、公式介绍 将一列数据X标准化到指定区间[a,b] (一)正向标准化公式 nor_X(b-a)*(X-X_…

VUE-列表

VUE-列表 列表功能 如下例子 列表展示 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv&qu…

网络分析工具

为了实现业务目标&#xff0c;每天都要在网络上执行大量操作&#xff0c;网络管理员很难了解网络中实际发生的情况、谁消耗的带宽最多&#xff0c;并分析是否正在发生任何可能导致带宽拥塞的活动。对于大型企业和分布式网络来说&#xff0c;这些挑战是多方面的&#xff0c;为了…

AI边缘计算盒子+ThingSense管理平台,推动明厨亮灶智慧监管新篇章

背景随着“互联网”时代的浪潮汹涌而至&#xff0c;国家及各地政府纷纷在“十四五”规划中明确指出&#xff0c;强化食品安全管理&#xff0c;利用技术手段实现智慧监管是刻不容缓的任务。为此&#xff0c;各地正加速推进“互联网明厨亮灶”的建设步伐&#xff0c;实现系统对接…

C# 报输入字符串格式不正确的原因

先放错误代码 23 class Voicewater 24 { 25 public void voicealarm(int tem) 26 { 27 Console.WriteLine("现在的温度是{}度了",tem); 28 } 29 } 解决方法…

14 Php学习:表单

表单 PHP 表单是用于收集用户输入的工具&#xff0c;通常用于网站开发。PHP 可以与 HTML 表单一起使用&#xff0c;用于处理用户提交的数据。通过 PHP 表单&#xff0c;您可以创建各种类型的表单&#xff0c;包括文本输入框、复选框、下拉菜单等&#xff0c;以便用户可以填写和…

Create an SAP Fiori App Using SAP Business Application Studio/连接时服务不可用

Create an SAP Fiori App Using SAP Business Application Studio 如果连接时遇到服务不可用 我们需要配置BTP上的连接。 参考文档 更改之后需要刷新 studio界面&#xff0c;重新选择就可以正常工作了

Linux 基于 UDP 协议的简单服务器-客户端应用

目录 一、socket编程接口 1、socket 常见API socket()&#xff1a;创建套接字 bind()&#xff1a;将用户设置的ip和port在内核中和我们的当前进程关联 listen() accept() 2、sockaddr结构 3、inet系列函数 二、UDP网络程序—发送消息 1、服务器udp_server.hpp initS…

【随笔】Git 高级篇 -- 远程服务器拒绝 git push reset(三十二)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

vue中使用水印

1. 在utils下创建watermark.js const watermark {}/**** param {要设置的水印的内容} str* param {需要设置水印的容器} container* param {需要设置水印的每一块的宽度} canWidth* param {需要设置水印的每一块的高度} canHeight* param {需要设置水印的字体} canFont* para…