Spring Boot框架下的单元测试
Spring Boot 提供了非常方便的测试支持,如 @SpringBootTest、@TestConfiguration 等注解,让开发者可以快速地在带有 Spring 容器上下文的环境中执行测试。mockMvc.perform(get("/users/1")) 可以模拟一次 GET 请求到 /users/1,并断言返回的 JSON 结构和内容。可以在 application-test.yml
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 {
@Test
void 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 {
@Mock
private UserRepository userRepository; // Mock出来的依赖
@InjectMocks
private UserService userService; // 要测试的对象,会把上面这个Mock自动注入进来
@Test
void 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);
}
@Test
void testGetUserNickname_notFound() {
// 没有设置when,则默认返回null
String 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 {
@Autowired
private 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 {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
// @MockBean的意思:Spring 启动时,
// 把真正的UserRepository替换成一个Mock对象,
// 我们就可以定义它的返回值,而不会真的连数据库
@Test
void 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);
}
@Test
void testGetUserNickname_notFound() {
// 不设置when就会返回null
String 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 {
@Autowired
private MockMvc mockMvc; // 用来模拟HTTP请求
@MockBean
private UserService userService; // Mock掉Service层
@Test
void 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 {
@Autowired
private TestRestTemplate restTemplate; // 可以真的发请求
@Test
void 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 {
@Mock
private MyRepository myRepository;
@InjectMocks
private 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,不要启动全部。
感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)