Spring Boot框架下的单元测试

embedded/2025/2/5 2:52:36/

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.ppmy.cn/embedded/159634.html

相关文章

ElasticSearch view

基础知识类 elasticsearch和数据库之间区别&#xff1f; elasticsearch&#xff1a;面向文档&#xff0c;数据以文档的形式存储&#xff0c;即JSON格式的对象。更强调数据的搜索、索引和分析。 数据库&#xff1a;更侧重于事务处理、数据的严格结构化和完整性&#xff0c;适用于…

用Python实现K均值聚类算法

在数据挖掘和机器学习领域&#xff0c;聚类是一种常见的无监督学习方法&#xff0c;用于将数据点划分为不同的组或簇。K均值聚类算法是其中一种简单而有效的聚类算法。今天&#xff0c;我将通过一个具体的Python代码示例&#xff0c;向大家展示如何实现K均值聚类算法&#xff0…

基于微信小程序的酒店管理系统设计与实现(源码+数据库+文档)

酒店管理小程序目录 目录 基于微信小程序的酒店管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员模块的实现 (1) 用户信息管理 (2) 酒店管理员管理 (3) 房间信息管理 2、小程序序会员模块的实现 &#xff08;1&#xff09;系统首页 &#xff…

【单细胞-第三节 多样本数据分析】

文件在单细胞\5_GC_py\1_single_cell\1.GSE183904.Rmd GSE183904 数据原文 1.获取临床信息 筛选样本可以参考临床信息 rm(list ls()) library(tinyarray) a geo_download("GSE183904")$pd head(a) table(a$Characteristics_ch1) #统计各样本有多少2.批量读取 学…

阿里云域名备案

一、下载阿里云App 手机应用商店搜索"阿里云",点击安装。 二、登录阿里云账号 三、打开"ICP备案" 点击"运维"页面的"ICP备案"。 四、点击"新增网站/App" 若无备案信息,则先新增备案信息。 五、开始备案

【三元锂电池SOH(State of Health)算法估算】

三元锂电池SOH&#xff08;State of Health&#xff09;算法估算是电池健康状态评估的重要内容。在此过程中&#xff0c;我们需要考虑电池的多种参数和因素。以下是一种可能的算法实现步骤&#xff1a; 数据采集&#xff1a;首先我们需要获取电池的实时数据&#xff0c;这可能…

udp和tcp的区别

目录 UDP 和 TCP 的区别 1. 连接性 2. 可靠性 3. 数据传输顺序 4. 流量控制和拥塞控制 5. 效率 6. 应用场景 UDP 和 TCP 的 C/C 代码实现区别 1. TCP 服务器端和客户端 TCP 服务器端&#xff08;Server&#xff09; TCP 客户端&#xff08;Client&#xff09; 2. U…

软件测试 - 概念篇

目录 1. 需求 1.1 用户需求 1.2 软件需求 2. 开发模型 2.1 软件的生命周期 2.2 常见开发模型 2.2.1 瀑布模型 2.2.2 螺旋模型 1. 需求 对于软件开发而言, 需求分为以下两种: 用户需求软件需求 1.1 用户需求 用户需求, 就是用户提出的需求, 没有经过合理的评估, 通常…