单元测试实战(二)Service 的测试
为鼓励单元测试,特分门别类示例各种组件的测试代码并进行解说,供开发人员参考。
本文中的测试均基于JUnit5。
单元测试实战(一)Controller 的测试
单元测试实战(二)Service 的测试
单元测试实战(三)JPA 的测试
单元测试实战(四)MyBatis-Plus 的测试
单元测试实战(五)普通类的测试
单元测试实战(六)其它
概述
与Controller不同,Service的测试可以脱离Spring上下文环境。这是因为Controller测试需要覆盖从HTTP请求到handler方法的路由,即需要SpringMvc的介入;而Service则是一种比较单纯的类,可以当做简单对象来测试。
我们将使用JUnit的MockitoExtension扩展来对Service对象进行测试。待测试对象为测试类的一个属性。测试仍遵循经典三段式:given、when、then;即:假设xxx……那么当yyy时……应该会zzz。
在每个测试之前应清理/重置测试数据,即操作的业务实体。
断言应主要检查Service的行为是否符合预期。
依赖
需要的依赖与Controller测试需要的依赖相同:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><scope>test</scope>
</dependency>
示例
以下是UserService的实现类UserServiceImpl。接口定义省略(从@Override注解不难推出)。
package com.aaa.api.auth.service.impl;import com.aaa.api.auth.entity.User;
import com.aaa.api.auth.repository.UserRepository;
import com.aaa.api.auth.service.UserService;
import org.springframework.stereotype.Service;import java.time.Instant;
import java.util.List;@Service
public class UserServiceImpl implements UserService {private final UserRepository repo;public UserServiceImpl(UserRepository repo) {this.repo = repo;}@Overridepublic User findById(Long id) {return repo.findById(id).orElse(null);}@Overridepublic User findByUserCode(String userCode) {return repo.findByUserCode(userCode).orElse(null);}@Overridepublic User save(User user) {user.setGmtModified(Instant.now());return repo.save(user);}@Overridepublic List<User> findAll() {return repo.findAll();}
}
以下是对UserServiceImpl进行测试的测试类:
package com.aaa.api.auth.service;import com.aaa.api.auth.entity.User;
import com.aaa.api.auth.repository.UserRepository;
import com.aaa.api.auth.service.impl.UserServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;import java.util.List;
import java.util.Optional;import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;@ExtendWith(MockitoExtension.class)
class UserServiceTest {@Mockprivate UserRepository repo;@InjectMocksprivate UserServiceImpl svc;private final User u1 = new User();private final User u2 = new User();private final User u3 = new User();@BeforeEachvoid setUp() {u1.setName("张三");u1.setUserCode("zhangsan");u1.setRole(User.ADMIN);u1.setEmail("zhangsan@aaa.net.cn");u1.setMobile("13600001234");u2.setName("李四");u2.setUserCode("lisi");u2.setRole(User.ADMIN);u2.setEmail("lisi@aaa.net.cn");u2.setMobile("13800001234");u3.setName("王五");u3.setUserCode("wangwu");u3.setRole(User.USER);u3.setEmail("wangwu@aaa.net.cn");u3.setMobile("13900001234");}@Testvoid testFindById() {// given - precondition or setupgiven(repo.findById(1L)).willReturn(Optional.of(u1));// when - action or the behaviour that we are going testUser found = svc.findById(1L);// then - verify the outputassertThat(found).isNotNull();assertThat(found.getUserCode()).isEqualTo("zhangsan");}@Testvoid testFindByIdNegative() {// given - precondition or setupgiven(repo.findById(1L)).willReturn(Optional.empty());// when - action or the behaviour that we are going testUser found = svc.findById(1L);// then - verify the outputassertThat(found).isNull();}@Testvoid testFindByUserCode() {// given - precondition or setupgiven(repo.findByUserCode(any())).willReturn(Optional.of(u1));// when - action or the behaviour that we are going testUser found = svc.findByUserCode("zhangsan");// then - verify the outputassertThat(found).isNotNull();assertThat(found.getUserCode()).isEqualTo("zhangsan");}@Testvoid testFindByUserCodeNegative() {// given - precondition or setupgiven(repo.findByUserCode(any())).willReturn(Optional.empty());// when - action or the behaviour that we are going testUser found = svc.findByUserCode("zhangsan");// then - verify the outputassertThat(found).isNull();}@Testvoid testSave() {// given - precondition or setupgiven(repo.save(any(User.class))).willAnswer((invocation -> invocation.getArguments()[0]));// when - action or the behaviour that we are going testUser saved = svc.save(u1);// then - verify the outputassertThat(saved).isNotNull();assertThat(saved.getGmtModified()).isNotNull();}@Testvoid testSaveNegative() {// given - precondition or setupgiven(repo.save(any())).willThrow(new RuntimeException("Testing"));// when - action or the behaviour that we are going test// User saved = svc.save(u1);// then - verify the outputassertThrows(RuntimeException.class, () -> svc.save(u1));}@Testvoid testFindAll() {// given - precondition or setupgiven(repo.findAll()).willReturn(List.of(u1, u2, u3));// when - action or the behaviour that we are going testList<User> found = svc.findAll();// then - verify the outputassertThat(found).isNotNull();assertThat(found.size()).isEqualTo(3);}
}
测试类说明:
第22行,我们使用了JUnit的MockitoExtension扩展。
第26行,我们Mock了一个UserRepository类型的对象repo,它是待测UserServiceImpl对象的依赖。由于脱离了Spring环境,所以它是个@Mock,不是@MockBean。
接着,第29行,就是待测对象svc。它有个注解@InjectMocks,意思是为该对象进行依赖注入(Mockito提供的功能);于是,repo就被注入到svc里了。
第31-33行提供了三个测试数据,并在setUp()方法中进行初始化/重置。@BeforeEach注解使得setUp()方法在每个测试之前都会执行一遍。
接下来,从56行开始,是测试方法;每个方法都遵循given - when - then三段式。
testFindById方法是测试根据id获取User对象的。它假设repository的findById(1)会返回对象u1;那么当调用svc.findById(1)时;返回的实体就应该是u1。
testFindByIdNegative方法是根据id获取User对象的负面测试。它假设找不到ID为1的User,即repository的findById(1)会返回空;那么当调用svc.findById(1)时;返回的实体应该为空。
testFindByUserCode、testFindByUserCodeNegative与testFindById、testFindByIdNegative一样,只不过查询条件换成userCode,不再赘述。
testSave方法是测试保存User对象的。它假设repository的save()方法在保存任何User对象时都会返回该对象本身;那么当调用svc.save(u1)时;返回的实体应该为u1。注意在这里我们assert了gmtModified属性,以确认UserServiceImpl.save()方法里对该属性的设置。
testSaveNegative方法是保存User对象的负面测试。它假设repository的save()方法会抛出运行时异常;那么当调用svc.save(u1)时;会接到这个异常。
testFindAll方法是测试获取所有User对象的,它假设repository的findAll()会返回对象u1、u2、u3;那么当调用svc.findAll()时;就应返回全部三个对象。
总结
Service的测试,推荐使用@ExtendWith(MockitoExtension.class),脱离Spring上下文,使用纯Mockito打桩。其它方面,理念均与Controller测试一样。
虽然Service测试的打桩器较简单,但由于业务逻辑可能都位于这一层,需要覆盖的场景多,测试用例也应该多。Service层的测试是所有层中最重要的。
相关文章:
单元测试实战(二)Service 的测试
为鼓励单元测试,特分门别类示例各种组件的测试代码并进行解说,供开发人员参考。 本文中的测试均基于JUnit5。 单元测试实战(一)Controller 的测试 单元测试实战(二)Service 的测试 单元测试实战&#x…...
LabVIEW和NIUSRP硬件加快了认知无线电开发
LabVIEW和NIUSRP硬件加快了认知无线电开发 对于电视频谱,主用户传输有两种类型:广播电视和节目制作和特殊事件(PMSE)设备。广播塔的位置已知,且覆盖电视传输塔(复用器)附近的某个特定地理区域(称为排除区域…...
嵌入式软件工程师面试题——2025校招社招通用(十六)
说明: 面试群,群号: 228447240面试题来源于网络书籍,公司题目以及博主原创或修改(题目大部分来源于各种公司);文中很多题目,或许大家直接编译器写完,1分钟就出结果了。但…...
白盒测试之测试用例设计方法
白盒测试之测试用例设计方法 什么是白盒测试白盒测试的特点白盒测试的设计方法静态设计方法动态设计方法语句覆盖分支(判定)覆盖条件覆盖判定条件覆盖组合覆盖路径覆盖总结 什么是白盒测试 按照测试方法分类,测试可以分为白盒测试和黑盒测试两种。 白盒测试也称结构…...
在CentOS 7上关闭SELinux
要在CentOS 7上关闭SELinux,可以按照以下步骤进行操作: 临时关闭SELinux(不建议使用): setenforce 0但是这种方式只对当前启动有效,重启系统后会失效。 2. 永久关闭SELinux: vi /etc/selinux…...
基于单片机温湿度PM2.5报警系统
**单片机设计介绍, 基于单片机温湿度PM2.5报警设置系统 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 单片机温湿度PM2.5报警设置系统是一种智能化的环境检测与报警系统。它主要由单片机、传感器、液晶显示屏、蜂鸣器…...
OpenHarmony系统编译环境
1. 推荐系统Ubuntu 2204 2. 必须安装的软件 apt-get install curl build-essential gcc g make ninja-build cmake libffi-dev e2fsprogs pkg-config flex bison perl bc openssl libssl-dev libelf-dev binutils binutils-dev libdwarf-dev u-boot-tools mtd-utils cpio de…...
二十三种设计模式全面解析-职责链模式(Chain of Responsibility Pattern):解放代码责任链,提升灵活性与可维护性
在软件开发中,我们经常面临处理请求或事件的情况。有时候,我们需要将请求或事件依次传递给多个对象进行处理,但又不确定哪个对象最终会处理它。这时候,职责链模式(Chain of Responsibility Pattern)就能派上…...
通过制作llama_cpp的docker镜像在内网离线部署运行大模型
对于机器在内网,无法连接互联网的服务器来说,想要部署体验开源的大模型,需要拷贝各种依赖文件进行环境搭建难度较大,本文介绍如何通过制作docker镜像的方式,通过llama.cpp实现量化大模型的快速内网部署体验。 一、llam…...
JavaScript 异步编程
异步的概念 异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。 在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序…...
linux课程第一课------命令的简单的介绍
作者前言 🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂 🎂 作者介绍: 🎂🎂 🎂 🎉🎉🎉…...
XLua热更新框架原理和代码实战
安装插件 下载Xlua插件:https://github.com/Tencent/xLua 下载完成后,把Asset文件夹下的文件拖入自己的工程Asset中,看到Unity编辑器上多了个Xlua菜单,说明插件导入成功 Lua启动代码 新建一个空场景,场景中什么都不…...
Hive客户端hive与beeline的区别
hive与beeline简介 1、背景2、hive3、beeline4、hive与beeline的关系 1、背景 Hive的hive与beeline命令都可以为客户端提供Hive的控制台连接。两者之间有什么区别或联系吗? Hive-cli(hive)是Hive连接hiveserver2的命令行工具,从Hive出生就一直存在&…...
<MySQL> 什么是数据库索引?数据库索引的底层结构是什么?
目录 一、什么是数据库索引? 1.1 索引的概念 1.2 索引的特点 1.3 索引的适用场景 1.4 索引的使用 1.4.1 创建索引 1.4.2 查看索引 1.4.3 删除索引 二、数据库索引的底层结构是什么? 2.1 数据库中的 B树 长啥样? 2.2 B树为什么适合做数据库索…...
对于koa中间件的理解
洋葱模型 大家都知道koa是洋葱模型,先一层一层通过next往下,之后再回去执行next后面的内容,next即使没写,最后也会进入下一个中间件。 那么什么是ctx呢,ctx顾名思义就是上下文,也就是上一层传给下一层的东…...
分页文件pagefile.sys引出的疑问
现象描述: 磁盘中显示无任何文件,却占用5GB左右的磁盘空间;格式化D盘时提示【此驱动器正在使用中。另一个程序或进程正在使用此驱动器。是否仍要对其进行格式化?】,点击【是】提示【Windows 无法完成格式化。】&#…...
【开题报告】疫苗在线预约小程序的设计与实现
1.选题背景 (1)新冠疫情下的疫苗接种挑战: 针对当前全球范围内的新冠疫情,疫苗接种成为控制疫情蔓延的重要手段。然而,大规模疫苗接种也带来了接种排队、人群聚集等管理难题,为了更好地组织和管理疫苗接种…...
【深度学习实验】注意力机制(二):掩码Softmax 操作
文章目录 一、实验介绍二、实验环境1. 配置虚拟环境2. 库版本介绍 三、实验内容0. 理论介绍a. 认知神经学中的注意力b. 注意力机制: 1. 注意力权重矩阵可视化(矩阵热图)2. 掩码Softmax 操作a. 导入必要的库b. masked_softmaxc. 实验结果 …...
idea运行项目之后一直卡在Writing classes… 解决方案
最近遇到idea里直接运行一个Spring boot项目后,idea一直慢悠悠的parsing java,然后就writing classes,然后就一直卡着不动了,运气好10几分钟能把项目启动起来。 多年的摸鱼经验告诉我,事出反常必有妖,赶紧…...
CentOS7 安装mysql8(离线安装)postgresql14(在线安装)
注:linux系统为vmware虚拟机,和真实工作环境可能有出入,不过正因如此我暴露了NAT转出的IP也没什么大碍 引言 postgresql与mysql目前都是非常受人欢迎的两大数据库,其各有各的优势,初学者先使用简单一张图来说明两者区…...
使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
浅谈 React Hooks
React Hooks 是 React 16.8 引入的一组 API,用于在函数组件中使用 state 和其他 React 特性(例如生命周期方法、context 等)。Hooks 通过简洁的函数接口,解决了状态与 UI 的高度解耦,通过函数式编程范式实现更灵活 Rea…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
CMake 从 GitHub 下载第三方库并使用
有时我们希望直接使用 GitHub 上的开源库,而不想手动下载、编译和安装。 可以利用 CMake 提供的 FetchContent 模块来实现自动下载、构建和链接第三方库。 FetchContent 命令官方文档✅ 示例代码 我们将以 fmt 这个流行的格式化库为例,演示如何: 使用 FetchContent 从 GitH…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
Springboot社区养老保险系统小程序
一、前言 随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱,社区养老保险系统小程序被用户普遍使用,为方…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
VisualXML全新升级 | 新增数据库编辑功能
VisualXML是一个功能强大的网络总线设计工具,专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑(如DBC、LDF、ARXML、HEX等),并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...
Linux 下 DMA 内存映射浅析
序 系统 I/O 设备驱动程序通常调用其特定子系统的接口为 DMA 分配内存,但最终会调到 DMA 子系统的dma_alloc_coherent()/dma_alloc_attrs() 等接口。 关于 dma_alloc_coherent 接口详细的代码讲解、调用流程,可以参考这篇文章,我觉得写的非常…...
