Spring Boot框架下的单元测试

1. 什么是单元测试

1.1 基本定义

  • 单元测试(Unit Test) 是对软件开发中最小可测单位(例如一个方法或者一个类)进行验证的一种测试方式。
  • 在 Java 后端的 Spring Boot 项目中,单元测试通常会借助 JUnitMockito 等框架对代码中核心逻辑进行快速且隔离的验证,保证功能正确性。

目的:及早发现并修复 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_PORTDEFINED_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 testgradle 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 后生成覆盖率报告,看你的测试是不是覆盖到了主要逻辑。
  • 测试很慢怎么办?
    • 如果你的逻辑不是必须要Spring,就尽量用纯Mock,不用 @SpringBootTest
    • 如果只是测Controller,就用 @WebMvcTest,不要启动全部。

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

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

相关文章

巧妙利用数据结构优化部门查询

目录 一、出现的问题 部门树接口超时 二、问题分析 源代码分析 三、解决方案 具体实现思路 四、优化的效果 一、出现的问题 部门树接口超时 无论是在A项目还是在B项目中&#xff0c;都存在类似的页面&#xff0c;其实就是一个部门列表或者叫组织列表。 从页面的展示形式…

【数据分析】案例04:豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)

豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask) 豆瓣电影Top250官网:https://movie.douban.com/top250写在前面 实验目的:实现豆瓣电影Top250详情的数据分析与Web网页可视化。电脑系统:Windows使用软件:PyCharm、NavicatPython版本:Python 3.…

【线程】基于环形队列的生产者消费者模型

1 环形队列 环形队列采用数组来模拟&#xff0c;用取模运算来模拟环状特性。 1.如何判断环形队列为空或者为满? 当环形队列为空时&#xff0c;头和尾都指向同一个位置。当环形队列为满时&#xff0c;头和尾也都指向同一个位置。 因此&#xff0c; 可以通过加计数器或者标记…

Vue指令v-html

目录 一、Vue中的v-html指令是什么&#xff1f;二、v-html指令与v-text指令的区别&#xff1f; 一、Vue中的v-html指令是什么&#xff1f; v-html指令的作用是&#xff1a;设置元素的innerHTML&#xff0c;内容中有html结构会被解析为标签。 二、v-html指令与v-text指令的区别…

OPENGLPG第九版学习 - 着色器基础

文章目录 2.1 着色器与OpenGL2.2 0penGL的可编程管线2.3 OpenGL着色语言GLSL概述2.3.1 使用GLSL构建着色器变量的声明变量的作用域变量的初始化构造函数 、 类型转换聚合类型访问向量和矩阵中的元素结构体数组多维数组 2.3.2 存储限制符const 存储限制符in 存储限制符out 存储限…

路径规划之启发式算法之二十九:鸽群算法(Pigeon-inspired Optimization, PIO)

鸽群算法(Pigeon-inspired Optimization, PIO)是一种基于自然界中鸽子群体行为的智能优化算法,由Duan等人于2014年提出。该算法模拟了鸽子在飞行过程中利用地标、太阳和磁场等导航机制的行为,具有简单、高效和易于实现的特点,适用于解决连续优化问题。 更多的仿生群体算法…

Docker Compose的使用

文章首发于我的博客&#xff1a;https://blog.liuzijian.com/post/docker-compose.html 目录 Docker Compose是什么Docker Compose安装Docker Compose文件Docker Compose常用命令案例&#xff1a;部署WordPress博客系统 Docker Compose是什么 Docker Compose是Docker官方的开源…

AP单类平均准确率

P_true N_true P_pred TP Fp N_pred FN TNP NTP&#xff08;真正样本&#xff0c;与真实框IoU大于阈值的框&#xff09; FP&#xff08;假正样本&#xff0c;与真实框IoU小于阈值的框&#xff09; TN&#xff08;真负样本&#xff0c;背景&#xff09;…

Leetcode—1427. 字符串的左右移【简单】Plus

2025每日刷题&#xff08;206&#xff09; Leetcode—1427. 字符串的左右移 实现代码 class Solution { public:string stringShift(string s, vector<vector<int>>& shift) {// shift[i] [dir, amount]// dir 0(左) or 1(右)// 左表示正, 右表示负int len…

机器学习10

自定义数据集 使用scikit-learn中svm的包实现svm分类 代码 import numpy as np import matplotlib.pyplot as pltclass1_points np.array([[1.9, 1.2],[1.5, 2.1],[1.9, 0.5],[1.5, 0.9],[0.9, 1.2],[1.1, 1.7],[1.4, 1.1]])class2_points np.array([[3.2, 3.2],[3.7, 2.9],…

股票入门知识

股票入门&#xff08;更适合中国宝宝体制&#xff09; 股市基础知识 本文介绍了股票的基础知识&#xff0c;股票的分类&#xff0c;各板块发行上市条件&#xff0c;股票代码&#xff0c;交易时间&#xff0c;交易规则&#xff0c;炒股术语&#xff0c;影响股价的因素&#xf…

Golang 并发机制-3:通道(channels)机制详解

并发编程是一种创建性能优化且响应迅速的软件的强大方法。Golang&#xff08;也称为 Go&#xff09;通过通道&#xff08;channels&#xff09;这一特性&#xff0c;能够可靠且优雅地实现并发通信。本文将揭示通道的概念&#xff0c;解释其在并发编程中的作用&#xff0c;并提供…

C#,入门教程(11)——枚举(Enum)的基础知识和高级应用

上一篇&#xff1a; C#&#xff0c;入门教程(10)——常量、变量与命名规则的基础知识https://blog.csdn.net/beijinghorn/article/details/123913570 不会枚举&#xff0c;就不会编程&#xff01; 枚举 一个有组织的常量系列 比如&#xff1a;一个星期每一天的名字&#xf…

读书笔记--分布式架构的异步化和缓存技术原理及应用场景

本篇是在上一篇的基础上&#xff0c;主要对分布式应用架构下的异步化机制和缓存技术进行学习&#xff0c;主要记录和思考如下&#xff0c;供大家学习参考。大家知道原来传统的单一WAR应用中&#xff0c;由于所有数据都在同一个数据库中&#xff0c;因此事务问题一般借助数据库事…

【C++】继承(下)

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解C的继承&#xff08;下&#xff09;&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 5.继承与友元6.继承与静态成员7.复杂的菱形继承及菱形虚拟继承8.继…

基于LLM的路由在专家混合应用:一种新颖的交易框架,该框架在夏普比率和总回报方面提升了超过25%

“LLM-Based Routing in Mixture of Experts: A Novel Framework for Trading” 论文地址&#xff1a;https://arxiv.org/pdf/2501.09636 摘要 随着深度学习和大语言模型&#xff08;LLMs&#xff09;的不断进步&#xff0c;混合专家&#xff08;MoE&#xff09;机制在股票投资…

Med-R2:基于循证医学的检索推理框架:提升大语言模型医疗问答能力的新方法

Med-R2 : Crafting Trustworthy LLM Physicians through Retrieval and Reasoning of Evidence-Based Medicine Med-R2框架Why - 这个研究要解决什么现实问题What - 核心发现或论点是什么How - 1. 前人研究的局限性How - 2. 你的创新方法/视角How - 3. 关键数据支持How - 4. 可…

【Blazor学习笔记】.NET Blazor学习笔记

我是大标题 我学习Blazor的顺序是基于Blazor University&#xff0c;然后实际内容不完全基于它&#xff0c;因为它的例子还是基于.NET Core 3.1做的&#xff0c;距离现在很遥远了。 截至本文撰写的时间&#xff0c;2025年&#xff0c;最新的.NET是.NET9了都&#xff0c;可能1…

C++ Primer 迭代器

欢迎阅读我的 【CPrimer】专栏 专栏简介&#xff1a;本专栏主要面向C初学者&#xff0c;解释C的一些基本概念和基础语言特性&#xff0c;涉及C标准库的用法&#xff0c;面向对象特性&#xff0c;泛型特性高级用法。通过使用标准库中定义的抽象设施&#xff0c;使你更加适应高级…

2 [GitHub遭遇严重供应链投毒攻击]

近日&#xff0c;有黑客针对 Discord Top.gg 的GitHub 账户发起了供应链攻击&#xff0c;此次攻击导致账户密码、凭证和其他敏感信息被盗&#xff0c;同时也影响到了大量开发人员。 Checkmarx 在一份技术报告中提到&#xff0c;黑客在这次攻击中使用了多种TTP&#xff0c;其中…