SpringBoot单元测试
Spring Boot 单元测试详解:从基础配置到 Web 安全测试实战
单元测试是保障代码质量的关键环节,Spring Boot 基于 Spring 单元测试框架提供了更简洁的测试支持,涵盖普通 Bean 测试、Web 接口测试、安全权限测试等场景。从 “Spring 传统测试 vs Spring Boot 测试→基础测试配置→Web 接口测试(MockMvc)→安全测试(Spring Security)” 四个维度,系统讲解 Spring Boot 单元测试的实现方法与最佳实践。
Spring 传统测试与 Spring Boot 测试的核心差异
在 Spring Boot 出现前,传统 Spring 项目的单元测试需要手动配置上下文、指定配置文件,步骤繁琐;而 Spring Boot 通过 @SpringBootTest 注解简化了配置,实现 “零 XML 配置” 的测试环境搭建。
| 对比维度 | 传统 Spring 测试 | Spring Boot 测试 |
|---|---|---|
| 核心注解 | @RunWith(SpringJUnit4ClassRunner.class) + @ContextConfiguration |
@RunWith(SpringRunner.class) + @SpringBootTest |
| 配置文件指定 | 需手动通过 locations 指定 XML/Java 配置(如 @ContextConfiguration(locations = "classpath:springmvc.xml")) |
自动扫描主程序类(@SpringBootApplication 标注类)的配置,无需手动指定 |
| Web 环境支持 | 需添加 @WebAppConfiguration 并手动初始化 MockMvc |
通过 @SpringBootTest(webEnvironment = ...) 快速指定 Web 环境类型(Mock / 真实容器) |
| 依赖简化 | 需手动引入 spring-test、junit 等依赖 |
引入 spring-boot-starter-test 一站式依赖,包含所有测试组件 |
Spring Boot 基础单元测试:环境搭建与普通 Bean 测试
Spring Boot 基础测试主要用于验证普通 Bean(如配置类、Service 类)的逻辑正确性,核心是通过 @SpringBootTest 自动加载 Spring 上下文,注入待测试 Bean。
引入测试依赖
Spring Boot 提供 spring-boot-starter-test 依赖,包含 JUnit、Spring Test、MockMvc 等核心测试组件,无需单独引入其他依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <!-- 仅测试环境生效 -->
</dependency>
编写基础测试类
场景:测试自定义配置类 CustomConfig 的属性注入是否正确
// 待测试的配置类(之前定义的自定义 YML 配置绑定类)
public class CustomConfig {
private String name;
private Map<String, String> typeFields;
}
// 测试类
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.*;
// 1. @RunWith(SpringRunner.class):指定测试运行器(Spring 提供,兼容 JUnit 4)
// 2. @SpringBootTest:自动加载 Spring 上下文(默认扫描主程序类所在包)
public class CustomConfigTest {
// 3. 注入待测试的 Bean
private CustomConfig customConfig;
// 4. 测试方法:验证配置属性是否正确注入
public void testConfigInjection() {
// 断言:name 属性不为 null,且值为 "订单服务"(custom.yml 中配置的值)
assertNotNull("配置 name 未注入", customConfig.getName());
assertEquals("配置 name 注入错误", "订单服务", customConfig.getName());
// 断言:typeFields 不为 null,且包含指定键值对
assertNotNull("配置 typeFields 未注入", customConfig.getTypeFields());
assertTrue("typeFields 应包含 key1", customConfig.getTypeFields().containsKey("key1"));
assertEquals("key1 的值错误", "value1", customConfig.getTypeFields().get("key1"));
}
}
3. @SpringBootTest 核心属性:webEnvironment
@SpringBootTest 的 webEnvironment 属性用于指定 Web 环境类型,默认值为 WebEnvironment.MOCK,适用于不同测试场景:
| WebEnvironment 类型 | 核心特点 | 适用场景 |
|---|---|---|
| MOCK(默认) | 加载 Web 上下文,提供 Mock Servlet 环境,不启动内嵌容器 | 测试 Controller 接口(无需真实端口,通过 MockMvc 模拟请求) |
| RANDOM_PORT | 启动内嵌容器(如 Tomcat),监听随机端口 | 测试真实 HTTP 请求(如跨服务调用、外部接口依赖) |
| DEFINED_PORT | 启动内嵌容器,监听配置文件中指定的端口(server.port) |
测试固定端口的场景(如与外部系统约定端口) |
| NONE | 仅加载 Spring 上下文,不提供 Web 环境 | 测试普通 Bean(如 Service、Repository),无 Web 依赖 |
Web 接口测试:使用 MockMvc 模拟 HTTP 请求
在 Web 项目中,需测试 Controller 接口的请求处理逻辑(如参数校验、响应状态、返回数据)。Spring Boot 提供 MockMvc 工具,可在不启动真实容器的情况下模拟 HTTP 请求(GET/POST/PUT/DELETE),高效验证接口正确性。
初始化 MockMvc
通过 @AutoConfigureMockMvc 自动配置 MockMvc,或手动通过 MockMvcBuilders 构建:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// 1. @AutoConfigureMockMvc:自动配置 MockMvc,无需手动初始化
// MOCK 环境,不启动容器
public class UserControllerTest {
// 2. 注入 MockMvc
private MockMvc mockMvc;
// 待测试的 Controller(示例)
// @RestController
// @RequestMapping("/user")
// public class UserController {
// @GetMapping("/{id}")
// public ResponseEntity<User> getUserById(@PathVariable Long id) {
// User user = new User(id, "张三", 25);
// return ResponseEntity.ok(user);
// }
// }
// 3. 测试 GET 接口:获取用户信息
public void testGetUserById() throws Exception {
mockMvc.perform(
// 模拟 GET 请求:/user/1
get("/user/{id}", 1)
.header("Authorization", "Bearer token123") // 模拟请求头
.param("name", "张三") // 模拟请求参数(Query String)
)
// 断言响应状态码为 200(OK)
.andExpect(status().isOk())
// 断言响应体的 JSON 字段(使用 jsonPath 表达式)
.andExpect(jsonPath("$.id").value(1)) // 响应体中 id 为 1
.andExpect(jsonPath("$.name").value("张三")) // 响应体中 name 为 "张三"
.andExpect(jsonPath("$.age").value(25)) // 响应体中 age 为 25
// 打印请求和响应详情(可选,调试用)
.andDo(result -> System.out.println("响应内容:" + result.getResponse().getContentAsString()));
}
}
MockMvc 核心 API 说明
| API 分类 | 核心方法 | 作用描述 |
|---|---|---|
| 请求构建 | MockMvcRequestBuilders.get/post/put/delete |
构建对应 HTTP 方法的请求,支持路径参数、请求头、请求体 |
| 响应断言 | MockMvcResultMatchers.status() |
断言响应状态码(如 isOk()=200、isBadRequest()=400) |
MockMvcResultMatchers.jsonPath() |
断言 JSON 响应体的字段值(如 $.name 匹配指定值) |
|
MockMvcResultMatchers.view() |
断言视图名称(适用于 thymeleaf/jsp 视图) | |
| 结果处理 | andDo(result -> ...) |
处理测试结果(如打印响应内容、日志记录) |
andReturn() |
获取测试结果对象(如响应体、请求信息) |
测试 POST 接口(带请求体)
import com.alibaba.fastjson.JSON;
import org.springframework.http.MediaType;
public void testCreateUser() throws Exception {
// 构建请求体(User 对象)
User newUser = new User(null, "李四", 30);
mockMvc.perform(
// 模拟 POST 请求:/user,请求体为 JSON
post("/user")
.contentType(MediaType.APPLICATION_JSON) // 指定请求体类型为 JSON
.content(JSON.toJSONString(newUser)) // 将对象转为 JSON 字符串
)
.andExpect(status().isCreated()) // 断言响应状态码为 201(Created)
.andExpect(jsonPath("$.name").value("李四"))
.andExpect(jsonPath("$.age").value(30));
}
Spring Security 安全测试:模拟认证用户
若项目集成了 Spring Security(权限控制),普通接口测试会因 “未认证” 被拦截。需通过 spring-security-test 依赖模拟认证用户,验证权限控制逻辑(如角色权限、接口访问控制)。
引入 Spring Security 测试依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
初始化 Security 支持的 MockMvc
通过 SecurityMockMvcConfigurers.springSecurity() 为 MockMvc 添加 Security 支持:
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
public class SecuredControllerTest {
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
// @Before:测试方法执行前初始化 MockMvc(添加 Security 支持)
.junit.Before
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(webApplicationContext)
// 关键:添加 Spring Security 支持,模拟认证
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
// 待测试的安全接口(示例)
// @RestController
// @RequestMapping("/admin")
// public class AdminController {
// @GetMapping("/dashboard")
// @PreAuthorize("hasRole('ADMIN')") // 仅 ADMIN 角色可访问
// public String getDashboard() {
// return "Admin Dashboard";
// }
// }
}
模拟认证用户(两种方式)
方式 1:@WithMockUser 注解(快速模拟用户)
@WithMockUser 注解用于快速创建一个 “虚拟认证用户”,无需真实数据库用户,适用于简单权限测试:
import org.springframework.security.test.context.support.WithMockUser;
// 模拟用户:用户名 root,密码 123456,角色 ADMIN
public void testAdminDashboardWithMockUser() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isOk()) // 认证通过,返回 200
.andExpect(content().string("Admin Dashboard")); // 响应内容正确
}
// 测试无权限用户访问(模拟普通用户,角色 USER)
public void testAdminDashboardWithNoPermission() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isForbidden()); // 无权限,返回 403(Forbidden)
}
方式 2:@WithUserDetails 注解(使用真实用户数据)
若需使用数据库中已存在的用户(通过 UserDetailsService 加载),可通过 @WithUserDetails 注解指定用户名,自动从数据库加载用户信息:
import org.springframework.security.test.context.support.WithUserDetails;
// 假设数据库中存在用户名为 "admin" 的用户,且角色为 ADMIN
// 自动调用 UserDetailsService.loadUserByUsername("admin") 加载用户
public void testAdminDashboardWithRealUser() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isOk())
.andExpect(content().string("Admin Dashboard"));
}
高级测试场景:Service 层与 Repository 层测试
除了 Controller 测试,Service 层(业务逻辑)和 Repository 层(数据访问)也是单元测试的重点,需结合 Mock 工具(如 Mockito)隔离外部依赖。
Service 层测试(使用 Mockito 模拟 Repository)
Service 层依赖 Repository 层(如 MyBatis、JPA),测试时需通过 @MockBean 模拟 Repository,避免依赖真实数据库:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals;
// 待测试的 Service
// @Service
// public class UserService {
// @Autowired
// private UserRepository userRepository;
//
// public String getUserNameById(Long id) {
// User user = userRepository.findById(id).orElseThrow(() -> new RuntimeException("用户不存在"));
// return user.getName();
// }
// }
// 无 Web 环境
public class UserServiceTest {
// 1. @MockBean:模拟 UserRepository(替代真实数据库访问)
private UserRepository userRepository;
// 2. 注入待测试的 Service
private UserService userService;
public void testGetUserNameById() {
// 3. 模拟 Repository 的返回结果(当调用 findById(1L) 时,返回指定 User 对象)
User mockUser = new User(1L, "王五", 28);
Mockito.when(userRepository.findById(1L)).thenReturn(java.util.Optional.of(mockUser));
// 4. 调用 Service 方法,验证逻辑正确性
String userName = userService.getUserNameById(1L);
assertEquals("Service 方法返回错误", "王五", userName);
// 5. 验证 Repository 方法是否被调用(可选,确保逻辑流程正确)
Mockito.verify(userRepository, Mockito.times(1)).findById(1L); // 确认 findById(1L) 被调用 1 次
}
// 测试用户不存在的场景
// 预期抛出 RuntimeException
public void testGetUserNameById_UserNotFound() {
// 模拟 Repository 返回空(用户不存在)
Mockito.when(userRepository.findById(99L)).thenReturn(java.util.Optional.empty());
// 调用 Service 方法,应抛出异常
userService.getUserNameById(99L);
}
}
Repository 层测试(使用 H2 内存数据库)
Repository 层测试需验证数据访问逻辑(如 SQL 语句、查询条件),可使用 H2 内存数据库(无需真实数据库,测试完成后数据自动清空):
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertTrue;
// @DataJpaTest:专门用于 JPA Repository 测试,自动配置 H2 数据库、EntityManager 等
public class UserRepositoryTest {
private UserRepository userRepository;
public void testFindByName() {
// 1. 插入测试数据(H2 内存数据库)
User user = new User(null, "赵六", 35);
userRepository.save(user);
// 2. 调用 Repository 方法查询
boolean exists = userRepository.existsByName("赵六");
// 3. 断言结果
assertTrue("Repository 查询错误,用户应存在", exists);
}
}
H2 数据库配置(application-test.yml):
spring:
datasource:
url: jdbc:h2:mem:testdb # 内存数据库 URL
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true # 启用 H2 控制台(测试时可访问 http://localhost:8082/h2-console)
jpa:
hibernate:
ddl-auto: create-drop # 测试完成后自动删除表结构
show-sql: true # 打印 SQL 语句(调试用)
单元测试最佳实践
- 测试隔离:每个测试方法独立运行,不依赖其他测试的结果(如使用
@Before初始化数据,@After清理数据); - 重点覆盖:优先测试核心业务逻辑(如 Service 层的复杂计算、权限控制),而非简单的 Getter/Setter 方法;
- 避免真实依赖:Service 测试用
@MockBean模拟 Repository,Controller 测试用MockMvc模拟 HTTP 请求,减少对外部系统(数据库、第三方接口)的依赖; - 断言明确:每个测试方法需包含至少一个断言(如
assertEquals、assertTrue),明确测试目标; - 命名规范:测试方法名应清晰表达测试场景(如
testGetUserById_UserNotFound表示 “测试用户不存在的场景”)。
总结
Spring Boot 单元测试通过 @SpringBootTest 简化了环境搭建,结合 MockMvc、Mockito、Spring Security Test 等工具,可覆盖从 Controller 到 Repository 的全链路测试:
- 基础测试:使用
@SpringBootTest测试普通 Bean,验证配置注入、业务逻辑; - Web 测试:使用
MockMvc模拟 HTTP 请求,测试 Controller 接口的响应状态和数据; - 安全测试:使用
@WithMockUser/@WithUserDetails模拟认证用户,验证权限控制; - 数据层测试:使用 H2 内存数据库测试 Repository,避免真实数据库依赖