测试驱动编程(4)模拟消除依赖

文章目录

    • 测试驱动编程(4)模拟消除依赖
      • 模拟框架Mockito
        • 什么要模拟
        • 名词解释
        • Mockito常用注解
        • Mockito常用静态方法
        • Mockito测试流程三部曲
        • 基础用法
        • 可变返回结果
        • 验证verfily
        • 对象监视spy
      • 示例实战
        • 升级版井字游戏
          • 需求一
          • 需求二
          • 需求三
      • 总结

测试驱动编程(4)模拟消除依赖

模拟框架Mockito

大道至简

什么要模拟

单元测试的要点就在于验证单个单元是否正常,而不考虑依赖,TDD中的单元测试尤其如此。
对于内部依赖,我们应该已对其进行测试过;对于外部依赖(JDK包),我们应该信任它们
消除依赖的两种手段:合理的设计和模拟实现,合理的设计与具体的业务有关,本次只介绍模拟实现如何去实践
下图是一个实际的关系,我们的目标就是通过模拟简化关系,方便测试

名词解释

测试替身的其它名字:哑元对象(dummy object)、测试存根(test stub)、测试间谍(test spy)、模拟对象(mock object)、伪造对象(fake object)

Mockito常用注解

使用的模拟框架是Mockito,它的常用注解如下:

  • @Mock:用于模拟的创建,使得测试类更具可读性(不调用真实方式,默认返回都是null),需要配对@ExtendWith(MockitoExtension.class)才能使用
  • @Spy:用于创建间谍实例,代替spy(Object)方法(调用真实方式)
  • @InjectMocks:用于自动实例化测试对象,并将所有的@Mock或@Spy注解字段依赖项注入其中(类似Spring框架中自动注入)
  • @Captor:用于创建参数捕获器

要处理所有上述注释,请MockitoAnnotations.initMocks(testClass); 必须至少使用一次。 要处理注释,我们可以使用内置的运行器MockitoJUnitRunner或规则MockitoRule 。 我们还可以在@Before注释的Junit方法中显式调用initMocks()方法。

Mockito常用静态方法

除了使用注解,我们还需要用到是它的三个主要静态方法:

  • mock():创建模拟对象,还可以使用when()和given()指定模拟行为
  • spy():实现部分模拟,调用实际的对象
  • verify():检查调用方法时提供的参数是否是指定参数,是一种断言
Mockito测试流程三部曲
  • 模拟:mock一个模拟对象

    模拟一个List对象,它会给所有方法添加基本实现,返回值和由方法的返回类型决定,如 int 会返回 0,布尔值返回 false。对于其他 type 会返回 null

  • 打桩:Stub打桩设置预期

    指定条件和预期返回

  • 验证:验证预期和实际值是否一致

基础用法
  • 无返回值,使用notify

  • 监视对象

  • 抛出异常

  • 模拟传入参数


    Mockito 提供 argument matchers 机制,例如 anyString() 匹配任何 String 参数,anyInt() 匹配任何 int 参数,anySet() 匹配任何 Set,any() 则意味着参数为任意值。自定义类型也可以,如 any(User.class)

可变返回结果

之前我们thenReturn 是返回结果都是写死的,如果要让被测试的方法不写死,返回实际结果并让我们可以获取到应该怎么做呢?
利用 InvocationOnMock 提供的方法可以获取 mock 方法的调用信息。下面是它提供的方法:

  • getArguments() 调用后会以 Object 数组的方式返回 mock 方法调用的参数
  • getMethod() 返回 java.lang.reflect.Method 对象
  • getMock() 返回 mock 对象
  • callRealMethod() 真实方法调用,如果 mock 的是接口它将会抛出异常
验证verfily

由程序员自己来决定验证结果,可以关注调用参数、返回结果、调用次数(times(0))
verify 也可以像 when 那样使用模拟参数,若方法中的某一个参数使用了matcher,则所有的参数都必须使用 matcher


在最后的验证时如果只输入字符串”hello”是会报错的,必须使用 Matchers 类内建的 eq 方法

对象监视spy

spy 的意思是你可以修改某个真实对象的某些方法的行为特征,而不改变他的基本行为特征

spy 保留了 list 的大部分功能,只是将它的 size() 方法改写了。不过 spy 在使用的时候有很多地方需要注意,一不小心就会导致问题,所以不到万不得已还是不要用 spy

示例实战

升级版井字游戏

“井字游戏”第二版的需求很简单:添加永久性存储,让玩家能够保存游戏的当前状态,以便以后接着玩。

需求一

作为玩家,我希望把当前下的棋能够保存起来,以便于我能看到下的历史记录
需求分析:需要保存的信息有轮次、X和Y坐标以及玩家
我这边打算以mongoDB数据库来进行持久化保存,选用什么数据库和测试驱动没什么关系
准备好依赖和环境配置:

-- 依赖
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
-- 配置
spring:data:mongodb:host: 192.168.3.112port: 27117database: tic-tac

我们的数据库叫tic-tac,并打算将数据保存在一个叫game的mongoDB集合中;接下来设计一个持久化类,用来保存游戏的相关数据
(示例项目源码链接将在文末给出,大家不用太关注技术细节,关注在功能和测试模拟上)

一般刚接触单元测试的开发人员的第一反应就是先初始化一个数据在数据库中,然后测试下能不能查到,类似下面代码:

上面是这些spring的相关配置和资源库自动注入,大家不用关心,和具体业务无关

1处表示构建一个游戏记录,然后将它保存到数据库中
2处表示根据唯一编号从数据库中取出游戏保存记录,检验下唯一编号是否对应的上

初看起来好像这个测试并没有什么问题,但是其实这里存在至少四个大问题:1 构建一个游戏记录本身就需要操作数据库,而且运行每个测试方法都要构建,很多数据库会导致主键冲突;2 构建记录本身需要依赖具体的数据库,需要配置一大堆额外的东西(数据库驱动、获取实例、进行连接、释放资源等等);3 万一有人修改了数据库的数据,测试用例将失败,你每次测试的时候得告诉别人别动我的数据!;4 因为设计到了数据库操作,要时刻保证你的数据库运行正常,因为测试用例往往很多,所以你还要忍耐相当长时间的数据库操作(本来单元测试都是在内存中快速运行的)

等等我们的目的就是要测试下井字游戏的逻辑,现在怎么好像变得在测试数据库了?数据库作为外部依赖我们原则上应该信任它才对呀,所以我们需要使用mockit来模拟数据库操作

所以我们如果只是测试下的一步棋被保存了,其实就是调用了资源库的保存方法即可,我们默认是信任资源库能保存成功的

模拟的对象的方式有二种一种是使用注解,一种是使用静态方法:


上面是静态方法方式


上面是注解的方式,需要在测试类上添加@ExtendWith(MockitoExtension.class)

井字游戏涉及到的几个类职责如下:

  • TicTacToeBean:存储游戏状态信息的实体类
  • TicTacService:提供存储服务,与资源库进行协作
  • TicTacRepository:对实体类数据进行持久化

下面是一个保存一步棋的测试示例,采用了注解的方式

注意:可以看到我们的测试类并没有使用@SpringBootTest注解,单元测试已经脱离了Spring上下文环境

1处模拟了资源库和服务,因为需要服务中需要调用资源库对数据库进行操作,所以需要将资源库注入到服务中,采用了@InjectMocks注解
2处是构建一个某步棋的状态信息
3处是调用服务保存下棋的信息
4处是验证,我们关心的是验证资源库的save方法是不是被调用了一次

因为还没有saveMove方法,所以编译直接报错了,这个是我们的的阶段,接着实现一个空方法,让编译通过

执行下测试用例后,发现报错了,因为和预期的不一样

预期是要产生一次数据库的save操作,结果实际上是0,然后实现刚才添加的空方法,让其变绿

跑下测试用了,发现变绿了,并查看下测试报告

我们目前只是测试了服务中的saveMove()方法,其实资源库的方法我们也应该测试下,由于项目中我们使用的基于SpringJpaData项目下的Spring Data MongoDB框架来进行操作数据库,底层实现和默认的方法都是遵循JPA规范的,不需要我们定义各种增删改查方法。

各种方法的返回值也确定,比如之前的Save方法,其接口如下:

所以我们测试成功和失败的时候分别返回的是当前保存对象以及空对象,示例代码如下:

跑下测试用例,验证我们的结果

我们发现其中需要的游戏状态信息是重复代码,所以可以重构下,重构完毕后记得验证下测试用例

需求二

刚才只是实现了其中一步游戏信息状态的保存,在游戏过程中,我们还需要保存每一步的信息,并且每次重新开始的时候要清空数据库

在实施需求二之前,我们先把第一版的测试用例加进来,运行保证其正确性

经过分析大概有这么几个阶段:

  • 游戏开始时,初始化一个全新的游戏状态信息对象,同时删除原来的游戏记录

以上两个方法比较简单和之前的类似,就直接给出代码了,不一小步一小步的讲解了,完成后一定要保证测试用例全部通过

  • 每当玩家下一步就保存起来

运行测试用例发现报错了

因为游戏的逻辑中目前我们还没有调用保存,经过分析,我们应该在setBoard()中保存,如果在play中编写,第一版的大量测试用例需要重构。

目前该方法已经具备了保存的所有参数信息(不熟悉该方法的需要回顾下井字游戏的第一版本,即该系列教程的第一篇)

当前的方法如下所示,接收的是多个参数,我们需要重构为一个TicTacBean

重构后如下:

play方法中调用的地方也应该做相应的修改:

运行所有的测试用例,第一版游戏的用例和第二版游戏用例都全部通过,大功告成

需求三

当玩家是继续游戏时,读取当前游戏的状态然后继续

作为玩家,我想保存当前游戏,以便于我下次可以继续上次的游戏

需要保存的游戏信息:棋盘以及最后的玩家,我这边就先不给出步骤了,请大家自行实现下

PS:代码仓库 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的jinggame2模块

总结

单元测试的本质就是要限定好单元的边界,如果单元测试的过程在为如何解析请求报文或数据库连接之类事情烦恼,那么你的单元测试很有可能超过边界了

为了在边界内快速响应结果做出业务实现,在涉及到数据库等第三方依赖的时候,会使用模拟的方式来解决。Mockito是一个优秀的模拟框架,在性能和灵活性上做到了很好的平衡,值得大家去熟悉其常用的API和注解

一个模拟测试过程有三步:1 模拟(对象);2 打桩(预期);3 验证(结果),然后结合红-绿-重构大法,就是良好的TDD实践之路。

我们都希望自己写的代码持续的焕发出活力,想持续焕发活力,你必须为以后重构提供坚实的基础。

敏捷大行其道的今天,TDD也是敏捷思想核心技术之环的一个重要环节,没有技术实践的敏捷注定是失败的,没有TDD为代表的测试驱动技术,技术之环同样也不够完善。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

YOLOv8架构详解

📌YOLOv8架构详解 YOLOv8 架构图YOLOv8 Backbone部分YOLOv8 Head部分Neck和Head结构 在视觉深度学习中,通常将模型分为 2~3 个组成部分:backbone、neck(可选) 和 head。 Backbone(主干网络)负责…

NTLite深度Windows系统镜像文件修改定制

计算机爱好者和技术宅的圈子里,NTLite是一个广受欢迎的名字,一款强大的Windows系统定制工具,允许用户对Windows安装镜像进行深度修改,从而打造出一个更加个性化、高效且精简的操作系统。无论是为了优化系统性能、移除不必要的组件,还是集成最新的更新和驱动,NTLite都能成…

继承与多态1

11.2(Person. Student. Employee、Faculty和Staff类)设计一个名为Person的类 和它的两个名 为Student和Employee子类。Employee类又有子类:教员类Faculty和职员类Staff,每个人都有姓名、地址、电话号码和电子邮件地址。学生有班级状态(大一,大二、大三或…

java后端框架-MyBatis

一、概述 1、起源 MyBatis本是Apache下的开源项目,名为iBatis,2010年转投谷歌,从iBatis3.x开始更名为MyBatis 2、优点 (1)优秀的数据持久层框架(对jdbc做了轻量级封装) 3、特点 (1)对jdbc中接口进行封装的同时还提供了一些自己的类实现…

计算机字符编码的发展

目录 背景 发展 第一阶段:ASCII编码 第二阶段:扩展ASCII编码 第三阶段:各国编码 第四阶段:Unicode编码 第五阶段:UTF系列编码方式 相关扩展 背景 在计算机诞生初期,所有的数据都是基于二进制数&am…

samba_ubuntu_share_vmbox_vmware

_____ Ubuntu 利用 samba 与 win 直接共享文件夹 _____ samba Samba - 维基百科,自由的百科全书 (wikipedia.org) 用于 win 和 unix 直接访问资源 samba 为选定的 unix 目录建立网络共享, 使得 win 用户可以像访问普通 win 下的文件夹那样来通过网络来…

npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本

安装npm时出现如下提示: 出现这个错误信息,是系统禁止执行PowerShell的脚本。 出现的原因是,系统默认的执行策略是Restricted(默认设置),限制执行,所以会出现如上提示。 解决方法:…

Linux服务器配置ssh证书登录

1、ssh证书登录介绍 Linux服务器ssh登录有密码登录和证书登录两种。如果使用密码登录,容易遭受密码泄露或者暴力破解,我们可以使用ssh证书登录并禁止使用密码登录,ssh证书登录通过公钥和私钥来完成整个连接过程,公钥保存在服务器…

Java中Arrays.toString与new String()字节数组使用的差异

Java 编程语言提供了许多内置方法和类,这使得程序员能够更加方便的处理数据和对象。本文将讨论 Arrays.toString 方法和 new String() 方法在处理字节数组时的不同。 文章目录 Arrays.toString 方法new String() 方法总结 Arrays.toString 方法 Arrays.toString() …

高维数组到向量的转换:两种方法的深度解析

新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、引言:高维数组的挑战与需求 二、方法一:使用NumPy库进行展平 示…

如何将md文件精确的转换成docx文件

如何将md文件转换成docx? 文章目录 如何将md文件转换成docx?一、如何将MD文件比较完美的转换成word呢?二、方法3 步骤1、下载一个可用的MarkDown编辑器2、下载Pandoc安装 三、来进行转化了 一、如何将MD文件比较完美的转换成word呢&#xff1…

从零开始学Vue3--根据目录结构自动生成路由

我们在测试或者小项目中经常遇到一个问题,就是加一个页面,就要在router.js中加一个路由,相当的麻烦,有没有办法可以根据目录结构自动生成路由呢? 想要自动生成路由,最重要的是能够获取指定目录下vue的路径…

开源代码分享(31)-计及电转气协同的含碳捕集与垃圾焚烧虚拟电厂优化调度

参考文献: [1]孙惠娟,刘昀,彭春华,等.计及电转气协同的含碳捕集与垃圾焚烧虚拟电厂优化调度[J].电网技术,2021,45(09):3534-3545.DOI:10.13335/j.1000-3673.pst.2020.1720. 1.摘要 为了促进多能源互补及能源低碳化,提出了计及电转气协同的含碳捕集与垃…

canfd与can2.0关系

canfd是can2.0的升级版, 支持canfd的设备就支持can2.0,但can2.0的设备不支持canfd 参考 是选CAN接口卡还是CANFD接口卡_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Hh411K7Zn/?spm_id_from333.999.0.0 哪些STM32有CANFD外设 STM32G0, STM…

Python 计算两组点云间的距离

两组点云间距离计算 一、介绍1.1 概念1.2 函数讲解二、代码示例三、结果示例一、介绍 1.1 概念 点云距离计算 :计算从源点云中每个点到目标点云中最近邻点的距离。 1.2 函数讲解 def compute_point_cloud_distance(self, target): # real signature unknown; restored from _…

使用OrangePi KunPeng Pro部署AI模型

目录 一、OrangePi Kunpeng Pro简介二、环境搭建三、模型运行环境搭建(1)下载Ollama用于启动并运行大型语言模型(2)配置ollama系统服务(3)启动ollama服务(4)启动ollama(5)查看ollama运行状态四、模型部署(1)部署1.8b的qwen(2)部署2b的gemma(3)部署3.8的phi3(4)部署4b的qwen(5)部…

工作中有哪些超级好用的C/C++程序库?

视频和讲义发布在这里: B站链接

Android Ktor 网络请求框架

Ktor 是一个由 JetBrains 开发的用于 Kotlin 编程语言的应用框架,旨在创建高性能的异步服务器和客户端应用程序。由于完全基于 Kotlin 语言,Ktor 能够让开发者编写出简洁、可读性强且功能强大的代码,特别适合那些已经熟悉 Kotlin 的开发人员。…

Python算法设计与分析期末

Python算法设计与分析期末通常涉及对算法基础知识的理解和应用,包括但不限于以下几个方面: 算法基础:了解算法的定义、特性(确定性、有穷性、可行性等)以及算法的分类。 时间复杂度和空间复杂度:学会分析算…

调试记录-U盘枚举失败之LPM影响

现象 板子接部分U盘出现枚举失败,看log像是硬件信号问题,如: [ 29.186464] usb usb3-port1: Cannot enable. Maybe the USB cable is bad? [ 30.079624] usb usb3-port1: Cannot enable. Maybe the USB cable is bad? [ 30.080200]…