0%

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-testjunit 等依赖 引入 spring-boot-starter-test 一站式依赖,包含所有测试组件

Spring Boot 基础单元测试:环境搭建与普通 Bean 测试

Spring Boot 基础测试主要用于验证普通 Bean(如配置类、Service 类)的逻辑正确性,核心是通过 @SpringBootTest 自动加载 Spring 上下文,注入待测试 Bean。

引入测试依赖

Spring Boot 提供 spring-boot-starter-test 依赖,包含 JUnit、Spring Test、MockMvc 等核心测试组件,无需单独引入其他依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <!-- 仅测试环境生效 -->
</dependency>

编写基础测试类

场景:测试自定义配置类 CustomConfig 的属性注入是否正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 待测试的配置类(之前定义的自定义 YML 配置绑定类)
@Data
@Component
@PropertySource(value = "classpath:custom.yml", factory = YmlPropertyFactory.class)
@ConfigurationProperties(prefix = "custom")
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 上下文(默认扫描主程序类所在包)
@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomConfigTest {

// 3. 注入待测试的 Bean
@Autowired
private CustomConfig customConfig;

// 4. 测试方法:验证配置属性是否正确注入
@Test
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

@SpringBootTestwebEnvironment 属性用于指定 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 构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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,无需手动初始化
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) // MOCK 环境,不启动容器
@AutoConfigureMockMvc
public class UserControllerTest {

// 2. 注入 MockMvc
@Autowired
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 接口:获取用户信息
@Test
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 接口(带请求体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.alibaba.fastjson.JSON;
import org.springframework.http.MediaType;

@Test
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 测试依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

初始化 Security 支持的 MockMvc

通过 SecurityMockMvcConfigurers.springSecurity() 为 MockMvc 添加 Security 支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class SecuredControllerTest {

@Autowired
private WebApplicationContext webApplicationContext;

private MockMvc mockMvc;

// @Before:测试方法执行前初始化 MockMvc(添加 Security 支持)
@org.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 注解用于快速创建一个 “虚拟认证用户”,无需真实数据库用户,适用于简单权限测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.security.test.context.support.WithMockUser;

@Test
// 模拟用户:用户名 root,密码 123456,角色 ADMIN
@WithMockUser(username = "root", password = "123456", roles = "ADMIN")
public void testAdminDashboardWithMockUser() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isOk()) // 认证通过,返回 200
.andExpect(content().string("Admin Dashboard")); // 响应内容正确
}

// 测试无权限用户访问(模拟普通用户,角色 USER)
@Test
@WithMockUser(roles = "USER")
public void testAdminDashboardWithNoPermission() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isForbidden()); // 无权限,返回 403(Forbidden)
}
方式 2:@WithUserDetails 注解(使用真实用户数据)

若需使用数据库中已存在的用户(通过 UserDetailsService 加载),可通过 @WithUserDetails 注解指定用户名,自动从数据库加载用户信息:

1
2
3
4
5
6
7
8
9
10
import org.springframework.security.test.context.support.WithUserDetails;

// 假设数据库中存在用户名为 "admin" 的用户,且角色为 ADMIN
@Test
@WithUserDetails("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,避免依赖真实数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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();
// }
// }

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // 无 Web 环境
public class UserServiceTest {

// 1. @MockBean:模拟 UserRepository(替代真实数据库访问)
@MockBean
private UserRepository userRepository;

// 2. 注入待测试的 Service
@Autowired
private UserService userService;

@Test
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 次
}

// 测试用户不存在的场景
@Test(expected = RuntimeException.class) // 预期抛出 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 内存数据库(无需真实数据库,测试完成后数据自动清空):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 等
@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryTest {

@Autowired
private UserRepository userRepository;

@Test
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):

1
2
3
4
5
6
7
8
9
10
11
12
13
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 语句(调试用)

单元测试最佳实践

  1. 测试隔离:每个测试方法独立运行,不依赖其他测试的结果(如使用 @Before 初始化数据,@After 清理数据);
  2. 重点覆盖:优先测试核心业务逻辑(如 Service 层的复杂计算、权限控制),而非简单的 Getter/Setter 方法;
  3. 避免真实依赖:Service 测试用 @MockBean 模拟 Repository,Controller 测试用 MockMvc 模拟 HTTP 请求,减少对外部系统(数据库、第三方接口)的依赖;
  4. 断言明确:每个测试方法需包含至少一个断言(如 assertEqualsassertTrue),明确测试目标;
  5. 命名规范:测试方法名应清晰表达测试场景(如 testGetUserById_UserNotFound 表示 “测试用户不存在的场景”)。

总结

Spring Boot 单元测试通过 @SpringBootTest 简化了环境搭建,结合 MockMvcMockitoSpring Security Test 等工具,可覆盖从 Controller 到 Repository 的全链路测试:

  1. 基础测试:使用 @SpringBootTest 测试普通 Bean,验证配置注入、业务逻辑;
  2. Web 测试:使用 MockMvc 模拟 HTTP 请求,测试 Controller 接口的响应状态和数据;
  3. 安全测试:使用 @WithMockUser/@WithUserDetails 模拟认证用户,验证权限控制;
  4. 数据层测试:使用 H2 内存数据库测试 Repository,避免真实数据库依赖

欢迎关注我的其它发布渠道