测试驱动编程(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.rhkb.cn/news/336276.html

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

相关文章

ue5 中ps使用记录贴

一、快捷键记录 放大图形 ctrlalt空格 放大图形 缩小视口 ctrl空格 ctrlD 取消选区 ctrlt缩小文字 w魔棒工具 选择魔棒的时候把容差打开的多一点 二、案例 移动文字 在相应的图层选择 移动文字 修改图片里的颜色 在通道里拷贝红色通道,复制红色通道粘贴给正常图…

【Linux】如何优雅的检查Linux上的用户登录、关机和重启日志

在诸如Ubuntu、Debian、Linux Mint、Fedora和Red Hat等广受欢迎的Linux发行版中,系统会忠实记录用户的登录、关机、重启以及运行时长信息。这些信息对管理员调查事件、排查故障或汇总用户活动报告极为宝贵。 Linux系统及应用程序日志通常保存在/var/log/目录下&…

【深度学习】吸烟行为检测软件系统

往期文章列表: 【YOLO深度学习系列】图像分类、物体检测、实例分割、物体追踪、姿态估计、定向边框检测演示系统【含源码】【深度学习】YOLOV8数据标注及模型训练方法整体流程介绍及演示【深度学习】行人跌倒行为检测软件系统【深度学习】火灾检测软件系统【深度学…

CVE-2020-7982 OpenWrt 远程命令执行漏洞学习(更新中)

OpenWrt是一款应用于嵌入式设备如路由器等的Linux操作系统。类似于kali等linux系统中的apt-get等,该系统中下载应用使用的是opgk工具,其通过非加密的HTTP连接来下载应用。但是其下载的应用使用了SHA256sum哈希值来进行检验,所以将下载到的数据…

window自动启动bat文件

开机自动开启远程桌面, WinR 执行netplwiz 命令进入设置;取消勾选,可选择所需用户,点击应用,输入远程的密码即可 开机自动开启远程桌面, WinR 执行netplwiz 命令进入设置;取消勾选&#xff0…

JVM(四)

在上一篇中,介绍了JVM组件中的运行时数据区域,这一篇主要介绍垃圾回收器 JVM架构图: 1、垃圾回收概述 在第一篇中介绍JVM特点时,有提到过内存管理,即Java语言相对于C,C进行的优化,可以在适当的…

探寻数据处理的高效之道:从Python内置方法到NumPy的飞跃

新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、引言:为什么要学习NumPy? 二、案例展示:创建整数序列…

爪哇,我初学乍道

>>上一篇(学校上课,是耽误我学习了。。) 2016年9月,我大二了。 自从我发现上课会耽误我学习,只要我认为不影响我期末学分的,我就逃课了。 绝大多数课都是要签到的,有的是老师突击喊名字…

专业的力量-在成为专家的道路上前进

专业的力量-在成为专家的道路上前进 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 现在稀缺的已不再是信息资源,而是运用信息的能力。过去…

前端 CSS 经典:filter 滤镜

前言:什么叫滤镜呢,就是把元素里的像素点通过一套算法转换成新的像素点,这就叫滤镜。而算法有 drop-shadow、blur、contrast、grayscale、hue-rotate 等。我们可以通过这些算法实现一些常见的 css 样式。 1. drop-shadow 图片阴影 可以用来…

甩掉接口文档烦恼!Spring Boot 集成 Knife4j,轻松玩转 API 可视化

一、引言:跟接口文档说拜拜 👋 作为一名 Java 开发者,你是否还在为编写繁琐的 API 文档而头疼?传统的手动编写方式不仅耗时费力,而且容易出错,难以维护。今天,我们就来介绍一款神器 Knife4j&am…

一款开箱即用的Markdown 编辑器!【送源码】

开源的 Markdown 编辑器 Cherry Markdown Editor 是一款前端-markdown-编辑器-组件,具有开箱即用、轻量简洁、易于扩展等特点,它可以运行在浏览器或服务端 (NodeJs). 当 Cherry Markdown 编辑器支持的语法不满足开发者需求时,可以快速的进行…

webstorm新建vue项目相关问题

前言 这个迭代后端需求偏少,前端code的键盘都起火星子了。来了4个外包支持,1个后端3个前端,还是不够用啊。刚好趁这个机会稍微学习下vue,其实之前环境也配置过了,所以这里就不分享环境配置了,主要分享下新建…

unity接入live2d

在bilibili上找到一个教程,首先注意一点,你直接导入那个sdk,并且打开示例,显示的模型是有问题的,你需要调整模型上脚本的一个枚举值,调整它的渲染顺序是front z to我看教程时候,很多老师都没有提…

Spring Cache自定义缓存key和过期时间

一、自定义全局缓存key和双冒号替换 使用 Redis的客户端 Spring Cache时,会发现生成 key中会多出一个冒号,而且有一个空节点的存在。 查看源码可知,这是因为 Spring Cache默认生成key的策略就是通过两个冒号来拼接。 同时 Spring Cache缓存…

01_Spring Ioc DI案例,setter方法和构造方法注入(详解) + 思维导图

文章目录 一.概念实操Maven父子工程 二. IOC和DI入门案例【重点】1 IOC入门案例【重点】问题导入1.1 门案例思路分析1.2 实现步骤2.1 DI入门案例思路分析2.2 实现步骤2.3 实现代码2.4 图解演示 三、Bean的基础配置问题导入问题导入1 Bean是如何创建的【理解】2 实例化Bean的三种…

STM32-GPIO八种输入输出模式

图片取自 江协科技 STM32入门教程-2023版 细致讲解 中文字幕 p5 【STM32入门教程-2023版 细致讲解 中文字幕】 https://www.bilibili.com/video/BV1th411z7sn/?p5&share_sourcecopy_web&vd_source327265f5c70f26411a53a9226af0b35c 目录 ​编辑 一.STM32的四种输…

MySQL之创建高性能的索引(六)

创建高性能的索引 选择合适的索引列顺序 当使用前缀索引的时候,在某些条件值的基数比正常值高的时候,问题就来了。例如,在某些应用程序中,对于没有登录的用户,都将其用户名记录为"guest",在记录…

LeetCode 264 —— 丑数 II

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 第一个丑数是 1 1 1,由于丑数的质因子只包含 2 、 3 、 5 2、3、5 2、3、5,所以后面的丑数肯定是前面的丑数分别乘以 2 、 3 、 5 2、3、5 2、3、5 后得到的数字。 这样,我…

ARM-V9 RME(Realm Management Extension)系统架构之系统能力的内存隔离和保护

安全之安全(security)博客目录导读 目录 一、内存隔离和保护 1、颗粒PAS过滤Granular PAS filtering 2、Cache的一致性维护 2.1 物理别名点 Point of Physical Aliasing (PoPA) 2.2 加密点 3、内存(DRAM)保护 3.1 内存加密和完整性 3.2 DRAM scrubbing 本博客探讨 RME…