纲要
- 使用
@SpringBootTest构建 Web 集成测试的基础环境 - 通过
MockMvc发起 HTTP 请求并断言响应 - 控制测试中是否启用
Spring Security安全检查 - 使用
@WithMockUser模拟已认证用户,简化安全测试 - 完整可运行的测试类代码示例
集成测试环境搭建
在 Spring Boot 应用中,spring-boot-starter-test 已经内置了对集成测试的支持。默认生成的项目结构中包含一个测试类,上面标注了 @SpringBootTest,它能够加载完整的 Spring 应用上下文,帮助我们执行端到端的集成测试。对于 Web 层,我们主要依赖 MockMvc 来模拟 HTTP 请求并对响应结果进行断言。
首先,保持测试包名与被测代码所在的包结构一致,例如被测 REST 控制器位于 com.example.rest,测试类也应该放在同名包下:
dir
src
└── test
└── java
└── com
└── example
└── rest
└── SecuredRestApiIntTest.java
测试类基本骨架如下:
java
package com.example.rest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
public class SecuredRestApiIntTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
public void shouldReturnOkWhenAccessApiWithoutSecurity() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isOk());
}
}
MockMvcBuilders.webAppContextSetup(context) 会基于完整的 Spring 应用上下文构建 MockMvc 实例。此时发起的请求会经过除了安全过滤器链之外的大部分 Spring MVC 基础设施(默认情况下不会应用安全配置)。
启用安全过滤链
如果希望集成测试也验证 Spring Security 的规则,可以在构建 MockMvc 时显式应用安全配置:
java
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
这样 MockMvc 就会加载完整的 Spring Security 过滤器链。此时再执行前面的测试,如果接口 /api/hello 被保护而请求中不携带认证信息,测试将会失败(例如返回 401 或 403)。
模拟已认证用户
为了在受保护的接口上便捷地进行测试,Spring Security Test 提供了 @WithMockUser 注解。它会在测试方法执行时创建一个虚拟的认证用户,绕过真实的登录流程,让我们专注于验证授权规则和业务逻辑。
java
import org.springframework.security.test.context.support.WithMockUser;
@Test
@WithMockUser(username = "zhangsan", roles = {"USER"})
public void shouldReturnOkWhenUserHasRoleUser() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isOk());
}
注解的可选属性包括:
| 属性 | 说明 | 默认值 |
|---|---|---|
username |
模拟用户的用户名 | "user" |
roles |
角色列表,会自动添加 ROLE_ 前缀 |
{"USER"} |
password |
密码(一般无需校验,可忽略) | "password" |
authorities |
细粒度权限列表,与 roles 二选一 |
无 |
更完整的测试类可能同时包含无需安全、需要安全但匿名、以及需要特定角色的多种场景:
java
package com.example.rest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
public class SecuredRestApiIntTest {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
public void shouldReturnUnauthorizedWhenNotAuthenticated() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "lisi", roles = {"ADMIN"})
public void shouldReturnForbiddenWhenUserLacksRole() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "wangwu", roles = {"USER"})
public void shouldReturnOkWhenUserHasCorrectRole() throws Exception {
mockMvc.perform(get("/api/hello"))
.andExpect(status().isOk());
}
}
上述代码清晰地展示了三个层次:未认证用户被拒绝,权限不足用户被禁止,拥有正确角色的用户才能正常访问。
测试流程图示
一个典型的集成安全测试执行过程如下:
Controller Spring Security 过滤器 MockMvc 测试类 Controller Spring Security 过滤器 MockMvc 测试类 #mermaid-svg-zHTdgH8ikWBQicO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zHTdgH8ikWBQicO7 .error-icon{fill:#552222;}#mermaid-svg-zHTdgH8ikWBQicO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zHTdgH8ikWBQicO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zHTdgH8ikWBQicO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zHTdgH8ikWBQicO7 .marker.cross{stroke:#333333;}#mermaid-svg-zHTdgH8ikWBQicO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zHTdgH8ikWBQicO7 p{margin:0;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zHTdgH8ikWBQicO7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .sequenceNumber{fill:white;}#mermaid-svg-zHTdgH8ikWBQicO7 #sequencenumber{fill:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-zHTdgH8ikWBQicO7 .messageText{fill:#333;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 .labelText,#mermaid-svg-zHTdgH8ikWBQicO7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .loopText,#mermaid-svg-zHTdgH8ikWBQicO7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-zHTdgH8ikWBQicO7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-zHTdgH8ikWBQicO7 .noteText,#mermaid-svg-zHTdgH8ikWBQicO7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-zHTdgH8ikWBQicO7 .actorPopupMenu{position:absolute;}#mermaid-svg-zHTdgH8ikWBQicO7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-zHTdgH8ikWBQicO7 .actor-man circle,#mermaid-svg-zHTdgH8ikWBQicO7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-zHTdgH8ikWBQicO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt未认证@WithMockUser提供认证 perform(get("/api/hello"))请求进入安全过滤器链401/403 响应断言状态码加载虚拟 Authentication请求转发到控制器200 响应200 响应断言 status().isOk()
通过 SecurityMockMvcConfigurers.springSecurity() 和 @WithMockUser 的组合,我们可以在不依赖真实用户数据库的情况下,灵活地测试各种安全场景。
关键依赖
确保 pom.xml 中包含以下测试依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
总结
本文基于 Spring Security 和 OAuth2 环境,介绍了使用 @SpringBootTest 与 MockMvc 编写 Web 集成测试的方法。
重点演示了如何在测试中启用安全过滤器,并通过 @WithMockUser 高效模拟认证用户,从而在保证安全测试覆盖的同时,极大降低了环境准备和数据维护的成本。
实战中建议将安全测试和非安全测试分离,以保持用例清晰、执行快速。