SpringBoot测试与安全
大约 6 分钟
SpringBoot测试与安全
SpringBoot测试概述
Spring Boot提供了全面的测试支持,包括单元测试、集成测试、Web层测试、数据访问层测试等。通过[spring-boot-starter-test](file:///Users/ldf/app/docs/pom.xml#L49-L53)起步依赖,可以轻松进行各种类型的测试。
测试支持的组件
单元测试
添加测试依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>服务层单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldCreateUser() {
// 准备测试数据
User inputUser = new User("张三", "zhangsan@example.com");
User savedUser = new User(1L, "张三", "zhangsan@example.com");
// 设置Mock行为
when(userRepository.save(inputUser)).thenReturn(savedUser);
// 执行测试
User result = userService.createUser(inputUser);
// 验证结果
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getName()).isEqualTo("张三");
assertThat(result.getEmail()).isEqualTo("zhangsan@example.com");
// 验证Mock调用
verify(userRepository).save(inputUser);
}
@Test
void shouldThrowExceptionWhenUserNotFound() {
// 设置Mock行为
when(userRepository.findById(999L)).thenReturn(Optional.empty());
// 执行测试并验证异常
assertThatThrownBy(() -> userService.getUserById(999L))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("用户未找到: 999");
}
}控制器单元测试
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUserById() throws Exception {
// 准备测试数据
User user = new User(1L, "张三", "zhangsan@example.com");
// 设置Mock行为
when(userService.getUserById(1L)).thenReturn(user);
// 执行测试
mockMvc.perform(get("/api/users/1")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("张三"))
.andExpect(jsonPath("$.email").value("zhangsan@example.com"));
// 验证Mock调用
verify(userService).getUserById(1L);
}
@Test
void shouldCreateUser() throws Exception {
// 准备测试数据
User inputUser = new User(null, "李四", "lisi@example.com");
User savedUser = new User(1L, "李四", "lisi@example.com");
// 设置Mock行为
when(userService.createUser(any(User.class))).thenReturn(savedUser);
// 执行测试
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"李四\",\"email\":\"lisi@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("李四"));
}
}集成测试
全局集成测试
@SpringBootTest
@TestPropertySource(locations = "classpath:application-test.properties")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ApplicationIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateAndRetrieveUser() {
// 准备测试数据
UserDto userDto = new UserDto();
userDto.setName("测试用户");
userDto.setEmail("test@example.com");
// 创建用户
ResponseEntity<User> createResponse = restTemplate.postForEntity(
"/api/users", userDto, User.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getId()).isNotNull();
// 获取用户
Long userId = createResponse.getBody().getId();
ResponseEntity<User> getResponse = restTemplate.getForEntity(
"/api/users/" + userId, User.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("测试用户");
}
}Web层集成测试
@WebMvcTest
class WebLayerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldHandleUserCreation() throws Exception {
User user = new User(1L, "测试用户", "test@example.com");
when(userService.createUser(any(User.class))).thenReturn(user);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "测试用户",
"email": "test@example.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L));
}
}数据访问层测试
JPA测试
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
void shouldFindUserByEmail() {
// 准备测试数据
User user = new User("张三", "zhangsan@example.com");
entityManager.persistAndFlush(user);
// 执行测试
User found = userRepository.findByEmail("zhangsan@example.com");
// 验证结果
assertThat(found).isNotNull();
assertThat(found.getName()).isEqualTo("张三");
}
@Test
void shouldFindUsersByNameContaining() {
// 准备测试数据
entityManager.persist(new User("张三", "zhangsan@example.com"));
entityManager.persist(new User("张四", "zhangsi@example.com"));
entityManager.persist(new User("李四", "lisi@example.com"));
entityManager.flush();
// 执行测试
List<User> users = userRepository.findByNameContainingIgnoreCase("张");
// 验证结果
assertThat(users).hasSize(2);
assertThat(users).extracting(User::getName)
.containsExactlyInAnyOrder("张三", "张四");
}
}数据库测试配置
# src/test/resources/application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.h2.console.enabled=true测试配置和工具
测试配置类
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public UserService mockUserService() {
return Mockito.mock(UserService.class);
}
@Bean
public Clock fixedClock() {
return Clock.fixed(Instant.parse("2023-01-01T10:00:00Z"), ZoneId.of("UTC"));
}
}测试工具类
public class TestUtil {
public static String asJsonString(final Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> T fromJsonString(final String json, Class<T> clazz) {
try {
return new ObjectMapper().readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}Spring Security安全框架
添加安全依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>基本安全配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/users/**").hasRole("USER")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults())
.csrf(csrf -> csrf.disable()); // 仅用于演示,生产环境应启用CSRF保护
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password("{noop}password") // {noop}表示不加密
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("{noop}admin")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}JWT安全配置
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
// JWT过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("无法设置用户认证", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}JWT工具类
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("无效的JWT签名");
} catch (MalformedJwtException ex) {
logger.error("无效的JWT令牌");
} catch (ExpiredJwtException ex) {
logger.error("过期的JWT令牌");
} catch (UnsupportedJwtException ex) {
logger.error("不支持的JWT令牌");
} catch (IllegalArgumentException ex) {
logger.error("JWT声明为空");
}
return false;
}
}安全测试
安全集成测试
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(locations = "classpath:application-test.properties")
class SecurityIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldAccessPublicEndpointWithoutAuthentication() {
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/public/info", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void shouldNotAccessProtectedEndpointWithoutAuthentication() {
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/users", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@WithMockUser(roles = "USER")
void shouldAccessProtectedEndpointWithAuthentication() {
ResponseEntity<String> response = restTemplate.getForEntity(
"/api/users", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}Web层安全测试
@WebMvcTest(UserController.class)
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUnauthorizedForProtectedEndpoint() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void shouldReturnUsersForAuthenticatedUser() throws Exception {
when(userService.getAllUsers()).thenReturn(Collections.emptyList());
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void shouldReturnForbiddenForAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAccessForAdminUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}JWT安全测试
@WebMvcTest(AuthController.class)
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AuthenticationManager authenticationManager;
@MockBean
private JwtTokenProvider tokenProvider;
@Test
void shouldAuthenticateUserAndReturnJwtToken() throws Exception {
LoginRequest loginRequest = new LoginRequest("user", "password");
Authentication authentication = Mockito.mock(Authentication.class);
when(authenticationManager.authenticate(any())).thenReturn(authentication);
when(tokenProvider.generateToken(authentication)).thenReturn("jwt-token");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(TestUtil.asJsonString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").value("jwt-token"))
.andExpect(jsonPath("$.tokenType").value("Bearer"));
}
}测试最佳实践
1. 测试分层
// 单元测试 - 只测试当前类的逻辑
@ExtendWith(MockitoExtension.class)
class UserServiceUnitTest {
// ...
}
// 集成测试 - 测试多个组件的协作
@SpringBootTest
class UserServiceIntegrationTest {
// ...
}
// Web层测试 - 测试HTTP接口
@WebMvcTest(UserController.class)
class UserControllerTest {
// ...
}2. 测试数据管理
@TestMethodOrder(OrderAnnotation.class)
class UserServiceTest {
@Test
@Order(1)
void shouldCreateUser() {
// 创建测试数据
}
@Test
@Order(2)
void shouldUpdateUser() {
// 使用之前创建的数据
}
}3. 测试配置
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
jpa:
hibernate:
ddl-auto: create-drop
show-sql: false
logging:
level:
org.springframework: WARN
com.example: DEBUG
app:
jwtSecret: testSecretKey
jwtExpirationInMs: 600004. 测试工具封装
public class TestDataProvider {
public static User createTestUser() {
return new User("测试用户", "test@example.com");
}
public static List<User> createTestUsers(int count) {
return IntStream.range(1, count + 1)
.mapToObj(i -> new User("用户" + i, "user" + i + "@example.com"))
.collect(Collectors.toList());
}
}通过以上内容,我们可以全面了解Spring Boot测试和安全的各个方面,包括单元测试、集成测试、Web层测试、数据访问层测试、Spring Security配置、JWT安全机制以及相关的测试最佳实践。合理的测试策略和安全配置是构建高质量、可维护应用的重要保障。
