Spring Boot框架下的单元测试
1. 什么是单元测试
1.1 基本定义
- 单元测试(Unit Test) 是对软件开发中最小可测单位(例如一个方法或者一个类)进行验证的一种测试方式。
- 在 Java 后端的 Spring Boot 项目中,单元测试通常会借助 JUnit、Mockito 等框架对代码中核心逻辑进行快速且隔离的验证,保证功能正确性。
目的:及早发现并修复 BUG,使后续迭代功能或重构时能迅速验证不会破坏已实现的功能。
1.2 单元测试在 Spring Boot 中的地位
- Spring Boot 提供了非常方便的测试支持,如
@SpringBootTest、@TestConfiguration等注解,让开发者可以快速地在带有 Spring 容器上下文的环境中执行测试。 - Spring Boot 本身也对 JUnit、Mockito、AssertJ 等常用测试框架或库提供了开箱即用的整合或依赖。
1.3 单元测试与其他测试的区别
- 单元测试:聚焦在一个方法或者一个类层面,不涉及过多外部依赖,能极快地发现逻辑错误。
- 集成测试:多个模块或组件交互时的测试,通常依赖真实数据库、消息队列等外部资源。
- 端到端测试(E2E):关注的是整个系统的完整流程,包括前端、后端、数据库、外部接口等。
- 在 Spring Boot 环境中,可以使用
@SpringBootTest搭配 Mock 或者内存数据库来实现集成测试,但这通常已经不只是“单元”级别了。
2. 为什么要写单元测试?
- 快速发现 Bug:写完代码马上测,不用等到上线才被发现问题。
- 减少回归成本:以后代码改动或升级,只要一键跑测试,就能知道改动有没有影响其他功能。
- 保证代码质量:养成单元测试的习惯,会促使你把代码设计得更简洁和更容易测试。
简单说:花小时间写单元测试,能为你省下大时间修 Bug。
3. 环境准备
3.1 依赖
在一个常规的 Spring Boot 项目中,只要在 pom.xml(Maven)或 build.gradle(Gradle) 里加上:
<!-- 如果是 Maven -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>
- JUnit 5:最常用的Java测试框架(写
@Test方法) - Mockito:常用的“模拟”库(用来Mock其他依赖)
- AssertJ / Hamcrest:更好用的断言库
- Spring Test / Spring Boot Test:Spring官方提供的测试辅助
这也就够了,一般不需要额外安装别的。
3.2 项目结构
Spring Boot常见的目录结构(Maven示例):
src├─ main│ └─ java│ └─ com.example.demo│ ├─ DemoApplication.java│ └─ service│ └─ MyService.java└─ test└─ java└─ com.example.demo├─ DemoApplicationTests.java└─ service└─ MyServiceTest.java
src/main/java放你的业务代码。src/test/java放你的测试代码。- 通常测试类的包路径要和被测类一致,这样在IDE里能很快对上号,也方便管理。
4. 最最简单的单元测试示例(不依赖Spring)
先从“纯JUnit”说起,最简单的情况就是:
- 我有一个普通的工具类/方法
- 我就想测试它的输入输出对不对
- 不用装载Spring,也不用什么复杂注解
代码示例
假设我们有一个简单的工具类:
public class MathUtil {public static int add(int a, int b) {return a + b;}
}
那我们写一个测试类(路径:src/test/java/.../MathUtilTest.java):
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;public class MathUtilTest {@Testvoid testAdd() {int result = MathUtil.add(2, 3);Assertions.assertEquals(5, result, "2 + 3 应该等于 5");}
}
@Test表示这是一个测试方法。Assertions.assertEquals(期望值, 实际值, "提示信息")用来断言。- 如果断言不通过,测试就失败;通过则测试成功。
运行方法:
- 在 IDE(如 IntelliJ/ Eclipse)里,右键这个
MathUtilTest类 -> Run 'MathUtilTest' - 或者在命令行里运行
mvn test(Maven) /gradle test(Gradle)。
这就是最最基础的单元测试。
5. 在 Spring Boot 里测试 - Service层
当你要测试一个 Service(业务逻辑类) 时,它可能依赖其他Bean(例如 Repository、Dao 等)或者需要 Autowired。在 Spring Boot 里,有两种主要方法:
方法1:纯Mock(不启动Spring Context)
适合只想测试这个Service逻辑本身,不需要真的连数据库,也不需要整个Spring环境。速度最快。
- 用 Mockito 来创建一个假的(Mock)依赖。
- 注入到要测的Service里,这样你可以控制依赖的行为。
示例
UserRepository.java (假设它是个接口,用来访问数据库):
public interface UserRepository {User findByName(String name);// ... 其他方法
}
UserService.java (我们要测这个类):
public class UserService {private UserRepository userRepository;// 通过构造注入依赖public UserService(UserRepository userRepository) {this.userRepository = userRepository;}public String getUserNickname(String name) {User user = userRepository.findByName(name);if (user == null) {return "UNKNOWN";}return user.getNickname();}
}
UserServiceTest.java (测试类,不依赖 Spring):
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.Mockito;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;@ExtendWith(MockitoExtension.class) // JUnit5 启用Mockito
public class UserServiceTest {@Mockprivate UserRepository userRepository; // Mock出来的依赖@InjectMocksprivate UserService userService; // 要测试的对象,会把上面这个Mock自动注入进来@Testvoid testGetUserNickname_found() {// 1. 假设我们模拟一个“数据库中查到的用户”:User mockUser = new User();mockUser.setName("alice");mockUser.setNickname("AliceWonder");// 2. 定义假数据的返回行为Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);// 3. 调用被测方法String nickname = userService.getUserNickname("alice");// 4. 断言结果Assertions.assertEquals("AliceWonder", nickname);}@Testvoid testGetUserNickname_notFound() {// 没有设置when,则默认返回nullString nickname = userService.getUserNickname("bob");Assertions.assertEquals("UNKNOWN", nickname);}
}
- 使用了
@Mock注解声明要模拟的依赖userRepository。 - 使用了
@InjectMocks注解告诉 Mockito,要把所有标记@Mock的对象注入进UserService。 - 这样就能让
UserService这个对象在执行时使用模拟过的userRepository而不访问真实数据库。 - 然后通过
Mockito.when(...)来定义依赖方法的返回值,用于测试用例的前提条件设置。 - 通过
Assertions来验证执行结果是否符合预期。
这样就只测 UserService 的逻辑,不会真的访问数据库,也不需要启动Spring,执行很快。
方法2:使用 @SpringBootTest (集成上下文)
适合你想在测试时使用Spring管理Bean,比如自动注入
@Autowired,或想测试和别的Bean的连接配置是否正常。
- 在测试类上加
@SpringBootTest。 - 这样Spring容器会启动,你也能
@Autowired你的Service或者别的Bean。
示例
UserService.java (类似前面,只不过换成了 Spring注入):
@Service
public class UserService {@Autowiredprivate UserRepository userRepository;public String getUserNickname(String name) {User user = userRepository.findByName(name);if (user == null) {return "UNKNOWN";}return user.getNickname();}
}
UserServiceSpringTest.java (测试类,使用Spring上下文):
@SpringBootTest
public class UserServiceSpringTest {@Autowiredprivate UserService userService;@MockBeanprivate UserRepository userRepository; // @MockBean的意思:Spring 启动时,// 把真正的UserRepository替换成一个Mock对象,// 我们就可以定义它的返回值,而不会真的连数据库@Testvoid testGetUserNickname_found() {User mockUser = new User();mockUser.setName("alice");mockUser.setNickname("AliceWonder");Mockito.when(userRepository.findByName("alice")).thenReturn(mockUser);String result = userService.getUserNickname("alice");Assertions.assertEquals("AliceWonder", result);}@Testvoid testGetUserNickname_notFound() {// 不设置when就会返回nullString result = userService.getUserNickname("unknown");Assertions.assertEquals("UNKNOWN", result);}
}
@SpringBootTest会启动一个小型Spring环境,让@Autowired能起作用。@MockBean可以让你把某个Bean(比如UserRepository)变成一个模拟对象。- 整体执行依然比较快,但比纯Mock稍微慢一点,因为要先启动Spring容器。
6. 测试 Controller 层
在 Spring Boot 里,Controller 是对外的 HTTP 接口。最常见的两种测试方式:
- 用
@WebMvcTest+MockMvc:不启动整个应用,只启动Web层,速度较快;- 用
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)+TestRestTemplate:会真正启动一个内嵌服务器,发起真实HTTP请求,更贴近实际环境。
6.1 @WebMvcTest 示例
@WebMvcTest(UserController.class) // 表示只测 UserController 相关
public class UserControllerTest {@Autowiredprivate MockMvc mockMvc; // 用来模拟HTTP请求@MockBeanprivate UserService userService; // Mock掉Service层@Testvoid testGetUser() throws Exception {// 假设Service返回一个User对象User mockUser = new User();mockUser.setName("test");mockUser.setNickname("TestNick");// 定义service行为Mockito.when(userService.getUserNickname("test")).thenReturn("TestNick");// 用MockMvc发起GET请求,对应Controller的 /user/{name} 路径mockMvc.perform(MockMvcRequestBuilders.get("/user/test")).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.content().string("TestNick"));}
}
@WebMvcTest只会扫描和加载 Web 层相关的组件,不会启动整个 Spring Boot 应用,测试速度更快。mockMvc.perform(get("/users/1"))可以模拟一次 GET 请求到/users/1,并断言返回的 JSON 结构和内容。
6.2 @SpringBootTest + TestRestTemplate
如果你想做一个更真实的集成测试(包括 Controller、Service、Repository 等所有层),可以使用 @SpringBootTest 并设置 webEnvironment = RANDOM_PORT 或 DEFINED_PORT 来启动内嵌服务器,然后注入 TestRestTemplate 来请求:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTest {@Autowiredprivate TestRestTemplate restTemplate; // 可以真的发请求@Testvoid testGetUser() {// 假设数据库里已经有对应数据,或者你用 @MockBean 替换依赖String result = restTemplate.getForObject("/user/test", String.class);Assertions.assertEquals("TestNick", result);}
}
- 这里会真正启动一个随机端口的Tomcat,然后
TestRestTemplate真的去请求本地这个/user/test接口。 - 非常贴近真实部署,只是适合做集成测试,比前面的MockMvc测试稍慢一点。
7. 常见的断言与技巧
7.1 断言
Assertions.assertEquals(期望, 实际):断言二者相等。Assertions.assertTrue(条件):断言条件为真。Assertions.assertThrows(异常类型, 代码块):断言执行代码块会抛出指定异常。
例如:
@Test
void testThrowException() {Assertions.assertThrows(IllegalArgumentException.class, () -> {// 假设调用了一个会抛出异常的方法someMethod(null);});
}
7.2 Mock时常用的 Mockito 方法
Mockito.when( mockObj.方法(...) ).thenReturn(返回值);Mockito.when( mockObj.方法(...) ).thenThrow(异常);Mockito.verify( mockObj, Mockito.times(1) ).某方法(...); // 验证是否调用了某方法
8. 测试运行与整合
8.1 在本地IDE里运行
- 右键单个测试类或测试方法 -> Run
- 或者在项目主目录运行
mvn test/gradle test
8.2 与持续集成(CI)整合
- 在 Jenkins、GitLab CI、GitHub Actions 等环境里,一般只要执行
mvn test或gradle test就可以跑所有测试用例。 - 如果测试全部通过,就说明代码基本没问题;如果测试挂了,说明你这次提交的改动有Bug或者破坏了原有逻辑。
9. 流程小结(简版“使用指南”)
-
新手首次写单元测试:
- 在
src/test/java下创建和源代码同包路径的测试类:XXXTest.java。 - 在类里加
@Test注解的方法,里面写Assertions.assertXXX(...)。 - 右键运行,看输出是否通过。
- 在
-
要测Service逻辑,但不想连数据库:
- 在测试类上写:
@ExtendWith(MockitoExtension.class) public class MyServiceTest {@Mockprivate MyRepository myRepository;@InjectMocksprivate MyService myService;... } - 用
Mockito.when(...)来模拟依赖。 - 用
assertEquals(...)来判断结果。
- 在测试类上写:
-
要测Service逻辑,并用Spring上下文:
- 在测试类上加
@SpringBootTest。 - 注入 Service:
@Autowired private MyService myService; - 如果你不想真的连数据库,那就用
@MockBean MyRepository myRepository;
- 在测试类上加
-
要测Controller:
- 用
@WebMvcTest(MyController.class)+@MockBean MyService myService;+MockMvc做单元测试,速度较快; - 或者用
@SpringBootTest(webEnvironment = ... )+TestRestTemplate做近似真实的集成测试。
- 用
10. 其他常见问题
- 测试和生产环境的配置冲突了怎么办?
- 可以在
application-test.yml里放测试专用配置,然后在测试时用spring.profiles.active=test。
- 可以在
- 需要数据库的测试怎么办?
- 可以用
@DataJpaTest+内存数据库(比如 H2),只测JPA相关逻辑,不影响真数据库。
- 可以用
- 想看覆盖率怎么办?
- 可以集成 Jacoco 插件,跑
mvn test后生成覆盖率报告,看你的测试是不是覆盖到了主要逻辑。
- 可以集成 Jacoco 插件,跑
- 测试很慢怎么办?
- 如果你的逻辑不是必须要Spring,就尽量用纯Mock,不用
@SpringBootTest。 - 如果只是测Controller,就用
@WebMvcTest,不要启动全部。
- 如果你的逻辑不是必须要Spring,就尽量用纯Mock,不用
相关文章:
Spring Boot框架下的单元测试
1. 什么是单元测试 1.1 基本定义 单元测试(Unit Test) 是对软件开发中最小可测单位(例如一个方法或者一个类)进行验证的一种测试方式。在 Java 后端的 Spring Boot 项目中,单元测试通常会借助 JUnit、Mockito 等框架对代码中核心逻辑进行快…...
git中文件的状态状态切换
在 Git 中,文件的状态是指文件相对于 Git 仓库的当前情况。以下是一些常见的文件状态及其含义: 未跟踪(Untracked): 这是新创建的文件或从其他位置复制过来的文件,Git 还没有开始跟踪这些文件的更改。 这些…...
基于脉冲响应不变法的IIR滤波器设计与MATLAB实现
一、设计原理 脉冲响应不变法是一种将模拟滤波器转换为数字滤波器的经典方法。其核心思想是通过对模拟滤波器的冲激响应进行等间隔采样来获得数字滤波器的单位脉冲响应。 设计步骤: 确定数字滤波器性能指标 将数字指标转换为等效的模拟滤波器指标 设计对应的模拟…...
RabbitMQ快速上手及入门
概念 概念: publisher:生产者,也就是发送消息的一方 consumer:消费者,也就是消费消息的一方 queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理 exchang…...
自动化构建-make/Makefile 【Linux基础开发工具】
文章目录 一、背景二、Makefile编译过程三、变量四、变量赋值1、""是最普通的等号2、“:” 表示直接赋值3、“?” 表示如果该变量没有被赋值,4、""和写代码是一样的, 五、预定义变量六、函数**通配符** 七、伪目标 .PHONY八、其他常…...
计算机网络之计算机网络的分类
计算机网络可以根据不同的角度进行分类,以下是几种常见的分类方式: 1. 按照规模和范围: 局域网(LAN,Local Area Network):覆盖较小范围(例如一个建筑物或校园)…...
MySQl的日期时间加
MySQL日期相关_mysql 日期加减-CSDN博客MySQL日期相关_mysql 日期加减-CSDN博客 raise notice 查询目标 site:% model:% date:% target:%,t_shipment_date.site,t_shipment_date.model,t_shipment_date.plant_date,v_date_shipment_qty_target;...
响应式编程与协程
响应式编程与协程的比较 响应式编程的弊端虚拟线程Java线程内核线程的局限性传统线程池的demo虚拟线程的demo 响应式编程的弊端 前面用了几篇文章介绍了响应式编程,它更多的使用少量线程实现线程间解耦和异步的作用,如线程的Reactor模型,主要…...
智能小区物业管理系统推动数字化转型与提升用户居住体验
内容概要 在当今快速发展的社会中,智能小区物业管理系统的出现正在改变传统的物业管理方式。这种系统不仅仅是一种工具,更是一种推动数字化转型的重要力量。它通过高效的技术手段,将物业管理与用户居住体验紧密结合,无疑为社区带…...
从Proxmox VE开始:安装与配置指南
前言 Proxmox Virtual Environment (Proxmox VE) 是一个开源的虚拟化平台,基于Debian Linux,支持KVM虚拟机和LXC容器。它提供了一个强大的Web管理界面,方便用户管理虚拟机、存储、网络等资源。Proxmox VE广泛应用于企业级虚拟化、云计算和开…...
【Docker项目实战】使用Docker部署MinIO对象存储(详细教程)
【Docker项目实战】使用Docker部署MinIO对象存储 前言一、 MinIO介绍1.1 MinIO简介1.2 主要特点1.3 主要使用场景二、本次实践规划2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、下载MinIO镜像五、…...
【C++】B2115 密码翻译
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C 文章目录 💯前言💯题目解析💯1. 老师的做法代码实现:思路解析: 💯2. 我的做法代码实现:思路分析: 💯3. 老师…...
02.04 数据类型
请写出以下几个数据的类型: 整数 a ----->int a的地址 ----->int* 存放a的数组b ----->int[] 存放a的地址的数组c ----->int*[] b的地址 ----->int* c的地址 ----->int** 指向printf函数的指针d ----->int (*)(const char*, ...) …...
Leetcode—598. 区间加法 II【简单】
2025每日刷题(206) Leetcode—598. 区间加法 II 实现代码 class Solution { public:int maxCount(int m, int n, vector<vector<int>>& ops) {int ans m * n;int x ops.size();if(ops.empty()) {return ans;}int xm ops[0][0], ym …...
AI浪潮下的IT从业者:危机、机遇与进化之路
目录 0. 前言1. 当前形势:站在十字路口1.1 AI的突飞猛进1.2 行业现状分析 2. 核心应对策略2.1 技术深度与广度的平衡2.2 人机协同的工作模式2.3 持续学习与创新 3. 结语 0. 前言 在人工智能快速发展的今天,IT从业者面临前所未有的挑战与机遇。本文将从实…...
OpenCV:图像轮廓
目录 简述 1. 什么是图像轮廓? 2. 查找图像轮廓 2.1 接口定义 2.2 参数说明 2.3 代码示例 2.4 运行结果 3. 绘制图像轮廓 3.1 接口定义 3.2 参数说明 3.3 代码示例 3.4 运行结果 4. 计算轮廓周长 5. 计算轮廓面积 6. 示例:计算图像轮廓的面…...
洛谷P11655「FAOI-R5」Lovely 139
P11655「FAOI-R5」Lovely 139 题目背景 Update:数据有 0 0,答案为 1,请选手特判以正常通过。 Height ≤ 139 \text{Height}\leq139 Height≤139。 题目描述 对于一个 01 \tt 01 01 串 S S S(下标从 1 1 1 开始)…...
文字显示省略号
多行文本溢出显示省略号...
Windows图形界面(GUI)-QT-C/C++ - QT Tab Widget
公开视频 -> 链接点击跳转公开课程博客首页 -> 链接点击跳转博客主页 目录 一、概述 1.1 什么是 QTabWidget? 1.2 使用场景 二、常见样式 2.1 选项卡式界面 2.2 动态添加和删除选项卡 2.3 自定义选项卡标题和图标 三、属性设置 3.1 添加页面&…...
C++11 多线程 锁与条件变量:mutex、lock_guard、unique_lock 和 condition_variable
文章目录 mutex核心成员函数使用场景 lock_guard功能和特性构造函数使用场景 unique_lock功能和特性构造函数核心成员函数使用场景 lock_guard对比unique_lockcondition_variable核心成员函数使用场景 mutex std::mutex 是 C 标准库中提供的一种互斥量,用于在多线程…...
【Proteus】NE555纯硬件实现LED呼吸灯效果,附源文件,效果展示
本文通过NE555定时器芯片和简单的电容充放电电路,设计了一种纯硬件实现的呼吸灯方案,并借助Proteus仿真软件验证其功能。方案无需编程,成本低且易于实现,适合电子爱好者学习PWM(脉宽调制)和定时器电路原理。 一、呼吸灯原理与NE555功能分析 1. 呼吸灯核心原理 呼吸灯的…...
Cosmos - 世界模型开发平台
文章目录 一、关于 Cosmos主要特点模型家族 二、使用示例1、推理2、后训练 许可证和联系方式 一、关于 Cosmos NVIDIA Cosmos是开发者第一的世界基础模型平台,旨在帮助物理AI开发者更好、更快地构建他们的物理AI系统。宇宙包含 预训练模型,可通过拥抱脸…...
图像分割中根据mask的ROI,去除mask和image中没有勾画ROI层数以外的图像
在分割任务中,一个患者有很多层图像,但是勾画的ROI仅有那么几层。我想去除ROI以外层数的那些没用的图像。这里以一个36张图像的nii格式数据为例 查看一下mask文件中有多少个非0图像 import nibabel as nib import numpy as np# 加载 .nii 文件 file_pat…...
【Java基础-42.3】Java 基本数据类型与字符串之间的转换:深入理解数据类型的转换方法
在 Java 开发中,基本数据类型与字符串之间的转换是非常常见的操作。无论是从用户输入中读取数据,还是将数据输出到日志或界面,都需要进行数据类型与字符串之间的转换。本文将深入探讨 Java 中基本数据类型与字符串之间的转换方法,…...
全栈开发:使用.NET Core WebAPI构建前后端分离的核心技巧(一)
目录 cors解决跨域 依赖注入使用 分层服务注册 缓存方法使用 内存缓存使用 缓存过期清理 缓存存在问题 分布式的缓存 cors解决跨域 前后端分离已经成为一种越来越流行的架构模式,由于跨域资源共享(cors)是浏览器的一种安全机制,它会阻止前端应用…...
springboot使用rabbitmq
使用springboot创建rabbitMQ的链接。 整个项目结构如下: 1.maven依赖 <dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>3.4.1</version> </dependency>application.y…...
Linux——ext2文件系统(二)
Linux——ext2文件系统 ext2文件系统宏观认识一、磁盘分区与格式化二、块组(Block Group)结构三、文件系统特性 文件名与目录名与inode一、inode的作用原理二、文件与目录名与inode的关系 路径一,路径解析二,路径缓存三࿰…...
动手学深度学习-3.2 线性回归的从0开始
以下是代码的逐段解析及其实际作用: 1. 环境设置与库导入 %matplotlib inline import random import torch from d2l import torch as d2l作用: %matplotlib inline:在 Jupyter Notebook 中内嵌显示 matplotlib 图形。random:生成…...
“深度强化学习揭秘:掌握DQN与PPO算法的精髓“
深度Q网络(Deep Q-Network,简称DQN)是一种结合了Q学习和深度神经网络的强化学习算法。它使用神经网络来近似Q值函数,从而实现对复杂状态空间中的动作选择。DQN的核心思想是通过贝尔曼方程(Bellman Equation)…...
如何让DeepSeek恢复联网功能?解决(由于技术原因,联网搜索暂不可用)
DeekSeek提示:(由于技术原因,联网搜索暂不可用) 众所周知,因为海外黑客的ddos攻击、僵尸网络攻击,deepseek的联网功能一直处于宕机阶段,但是很多问题不联网出来的结果都还是2023年的,…...
