6.1 测试框架概述
Spring Boot测试特性
Spring Boot提供了全面的测试支持:
- 测试切片(Test Slices):针对特定层的测试
- 自动配置:测试环境自动配置
- 测试工具:丰富的测试工具和注解
- 模拟支持:Mock和Spy支持
- 集成测试:完整的应用上下文测试
测试依赖
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Testcontainers for integration testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
<!-- WireMock for external service mocking -->
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
测试配置
# 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=
# JPA配置
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 日志配置
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
# 禁用安全
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
6.2 单元测试
Service层测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
@DisplayName("创建用户 - 成功")
void createUser_Success() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("testuser")
.email("test@example.com")
.password("password123")
.firstName("Test")
.lastName("User")
.build();
User expectedUser = User.builder()
.id(1L)
.username("testuser")
.email("test@example.com")
.password("encodedPassword")
.firstName("Test")
.lastName("User")
.build();
when(userRepository.existsByUsername("testuser")).thenReturn(false);
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(passwordEncoder.encode("password123")).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(expectedUser);
// When
User result = userService.createUser(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
assertThat(result.getUsername()).isEqualTo("testuser");
assertThat(result.getEmail()).isEqualTo("test@example.com");
assertThat(result.getPassword()).isEqualTo("encodedPassword");
verify(userRepository).existsByUsername("testuser");
verify(userRepository).existsByEmail("test@example.com");
verify(passwordEncoder).encode("password123");
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail("test@example.com", "Test User");
}
@Test
@DisplayName("创建用户 - 用户名已存在")
void createUser_UsernameExists() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("existinguser")
.email("test@example.com")
.password("password123")
.build();
when(userRepository.existsByUsername("existinguser")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(request))
.isInstanceOf(UserAlreadyExistsException.class)
.hasMessage("Username already exists: existinguser");
verify(userRepository).existsByUsername("existinguser");
verify(userRepository, never()).save(any(User.class));
}
@Test
@DisplayName("查找用户 - 成功")
void findUserById_Success() {
// Given
Long userId = 1L;
User expectedUser = User.builder()
.id(userId)
.username("testuser")
.email("test@example.com")
.build();
when(userRepository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When
User result = userService.findById(userId);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(userId);
assertThat(result.getUsername()).isEqualTo("testuser");
verify(userRepository).findById(userId);
}
@Test
@DisplayName("查找用户 - 用户不存在")
void findUserById_NotFound() {
// Given
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.findById(userId))
.isInstanceOf(UserNotFoundException.class)
.hasMessage("User not found with id: 999");
verify(userRepository).findById(userId);
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "ab", "verylongusernamethatexceedslimit"})
@DisplayName("验证用户名 - 无效用户名")
void validateUsername_Invalid(String username) {
// When & Then
assertThatThrownBy(() -> userService.validateUsername(username))
.isInstanceOf(IllegalArgumentException.class);
}
@ParameterizedTest
@CsvSource({
"test@example.com, true",
"invalid-email, false",
"@example.com, false",
"test@, false"
})
@DisplayName("验证邮箱格式")
void validateEmail(String email, boolean expected) {
// When
boolean result = userService.isValidEmail(email);
// Then
assertThat(result).isEqualTo(expected);
}
}
Repository层测试
@DataJpaTest
@TestPropertySource(properties = {
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.datasource.url=jdbc:h2:mem:testdb"
})
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
@Test
@DisplayName("根据用户名查找用户")
void findByUsername_Success() {
// Given
User user = User.builder()
.username("testuser")
.email("test@example.com")
.password("password")
.build();
entityManager.persistAndFlush(user);
// When
Optional<User> result = userRepository.findByUsername("testuser");
// Then
assertThat(result).isPresent();
assertThat(result.get().getUsername()).isEqualTo("testuser");
assertThat(result.get().getEmail()).isEqualTo("test@example.com");
}
@Test
@DisplayName("根据邮箱查找用户")
void findByEmail_Success() {
// Given
User user = User.builder()
.username("testuser")
.email("test@example.com")
.password("password")
.build();
entityManager.persistAndFlush(user);
// When
Optional<User> result = userRepository.findByEmail("test@example.com");
// Then
assertThat(result).isPresent();
assertThat(result.get().getEmail()).isEqualTo("test@example.com");
}
@Test
@DisplayName("检查用户名是否存在")
void existsByUsername() {
// Given
User user = User.builder()
.username("existinguser")
.email("existing@example.com")
.password("password")
.build();
entityManager.persistAndFlush(user);
// When & Then
assertThat(userRepository.existsByUsername("existinguser")).isTrue();
assertThat(userRepository.existsByUsername("nonexistentuser")).isFalse();
}
@Test
@DisplayName("分页查询活跃用户")
void findActiveUsers() {
// Given
User activeUser1 = User.builder()
.username("active1")
.email("active1@example.com")
.password("password")
.status(UserStatus.ACTIVE)
.build();
User activeUser2 = User.builder()
.username("active2")
.email("active2@example.com")
.password("password")
.status(UserStatus.ACTIVE)
.build();
User inactiveUser = User.builder()
.username("inactive")
.email("inactive@example.com")
.password("password")
.status(UserStatus.INACTIVE)
.build();
entityManager.persist(activeUser1);
entityManager.persist(activeUser2);
entityManager.persist(inactiveUser);
entityManager.flush();
// When
Pageable pageable = PageRequest.of(0, 10);
Page<User> result = userRepository.findByStatus(UserStatus.ACTIVE, pageable);
// Then
assertThat(result.getContent()).hasSize(2);
assertThat(result.getContent())
.extracting(User::getUsername)
.containsExactlyInAnyOrder("active1", "active2");
}
@Test
@DisplayName("自定义查询 - 根据创建时间范围查找用户")
void findUsersByCreatedAtBetween() {
// Given
LocalDateTime start = LocalDateTime.now().minusDays(7);
LocalDateTime end = LocalDateTime.now();
User oldUser = User.builder()
.username("olduser")
.email("old@example.com")
.password("password")
.build();
// 手动设置创建时间
entityManager.persist(oldUser);
entityManager.flush();
// 使用原生SQL更新创建时间
entityManager.getEntityManager()
.createNativeQuery("UPDATE users SET created_at = ? WHERE id = ?")
.setParameter(1, start.minusDays(10))
.setParameter(2, oldUser.getId())
.executeUpdate();
User recentUser = User.builder()
.username("recentuser")
.email("recent@example.com")
.password("password")
.build();
entityManager.persistAndFlush(recentUser);
// When
List<User> result = userRepository.findByCreatedAtBetween(start, end);
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).getUsername()).isEqualTo("recentuser");
}
}
6.3 集成测试
Web层测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class UserControllerIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
@DisplayName("创建用户 - 集成测试")
void createUser_IntegrationTest() {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("integrationtest")
.email("integration@example.com")
.password("password123")
.firstName("Integration")
.lastName("Test")
.build();
// When
ResponseEntity<ApiResponse<UserResponse>> response = restTemplate.postForEntity(
"/api/users", request,
new ParameterizedTypeReference<ApiResponse<UserResponse>>() {});
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().isSuccess()).isTrue();
UserResponse userResponse = response.getBody().getData();
assertThat(userResponse.getUsername()).isEqualTo("integrationtest");
assertThat(userResponse.getEmail()).isEqualTo("integration@example.com");
// 验证数据库中的数据
Optional<User> savedUser = userRepository.findByUsername("integrationtest");
assertThat(savedUser).isPresent();
assertThat(savedUser.get().getEmail()).isEqualTo("integration@example.com");
}
@Test
@DisplayName("获取用户列表 - 分页测试")
void getUserList_PaginationTest() {
// Given - 创建测试数据
for (int i = 1; i <= 15; i++) {
User user = User.builder()
.username("user" + i)
.email("user" + i + "@example.com")
.password(passwordEncoder.encode("password"))
.build();
userRepository.save(user);
}
// When - 请求第一页,每页10条
ResponseEntity<ApiResponse<Page<UserResponse>>> response = restTemplate.exchange(
"/api/users?page=0&size=10&sort=username,asc",
HttpMethod.GET,
null,
new ParameterizedTypeReference<ApiResponse<Page<UserResponse>>>() {});
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
Page<UserResponse> page = response.getBody().getData();
assertThat(page.getContent()).hasSize(10);
assertThat(page.getTotalElements()).isEqualTo(15);
assertThat(page.getTotalPages()).isEqualTo(2);
assertThat(page.getContent().get(0).getUsername()).isEqualTo("user1");
}
@Test
@DisplayName("用户认证流程测试")
void userAuthenticationFlow() {
// Given - 创建用户
User user = User.builder()
.username("authtest")
.email("auth@example.com")
.password(passwordEncoder.encode("password123"))
.build();
userRepository.save(user);
// When - 登录
LoginRequest loginRequest = new LoginRequest("authtest", "password123");
ResponseEntity<ApiResponse<JwtResponse>> loginResponse = restTemplate.postForEntity(
"/api/auth/login", loginRequest,
new ParameterizedTypeReference<ApiResponse<JwtResponse>>() {});
// Then - 验证登录响应
assertThat(loginResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(loginResponse.getBody()).isNotNull();
JwtResponse jwtResponse = loginResponse.getBody().getData();
assertThat(jwtResponse.getToken()).isNotBlank();
assertThat(jwtResponse.getUsername()).isEqualTo("authtest");
// When - 使用token访问受保护资源
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(jwtResponse.getToken());
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<ApiResponse<UserResponse>> profileResponse = restTemplate.exchange(
"/api/user/profile",
HttpMethod.GET,
entity,
new ParameterizedTypeReference<ApiResponse<UserResponse>>() {});
// Then - 验证受保护资源访问
assertThat(profileResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(profileResponse.getBody().getData().getUsername()).isEqualTo("authtest");
}
}
MockMvc测试
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private JwtTokenUtil jwtTokenUtil;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("创建用户 - Web层测试")
@WithMockUser(roles = "ADMIN")
void createUser_WebLayerTest() throws Exception {
// Given
CreateUserRequest request = CreateUserRequest.builder()
.username("webtest")
.email("web@example.com")
.password("password123")
.firstName("Web")
.lastName("Test")
.build();
User createdUser = User.builder()
.id(1L)
.username("webtest")
.email("web@example.com")
.firstName("Web")
.lastName("Test")
.build();
when(userService.createUser(any(CreateUserRequest.class))).thenReturn(createdUser);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.username").value("webtest"))
.andExpect(jsonPath("$.data.email").value("web@example.com"))
.andDo(print());
verify(userService).createUser(any(CreateUserRequest.class));
}
@Test
@DisplayName("获取用户 - 参数验证测试")
void getUser_ValidationTest() throws Exception {
// When & Then - 测试无效ID
mockMvc.perform(get("/api/users/{id}", "invalid"))
.andExpect(status().isBadRequest())
.andDo(print());
}
@Test
@DisplayName("创建用户 - 请求体验证测试")
void createUser_RequestValidationTest() throws Exception {
// Given - 无效请求
CreateUserRequest invalidRequest = CreateUserRequest.builder()
.username("") // 空用户名
.email("invalid-email") // 无效邮箱
.password("123") // 密码太短
.build();
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.errors").isArray())
.andDo(print());
}
@Test
@DisplayName("搜索用户 - 查询参数测试")
@WithMockUser
void searchUsers_QueryParametersTest() throws Exception {
// Given
List<User> users = Arrays.asList(
User.builder().id(1L).username("user1").email("user1@example.com").build(),
User.builder().id(2L).username("user2").email("user2@example.com").build()
);
Page<User> userPage = new PageImpl<>(users, PageRequest.of(0, 10), 2);
when(userService.searchUsers(anyString(), any(Pageable.class))).thenReturn(userPage);
// When & Then
mockMvc.perform(get("/api/users/search")
.param("keyword", "user")
.param("page", "0")
.param("size", "10")
.param("sort", "username,asc"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.content").isArray())
.andExpect(jsonPath("$.data.content").value(hasSize(2)))
.andExpect(jsonPath("$.data.totalElements").value(2))
.andDo(print());
}
}
6.4 性能测试
JMeter测试计划
<!-- user-api-test-plan.jmx -->
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.4.1">
<hashTree>
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="User API Performance Test">
<stringProp name="TestPlan.comments">用户API性能测试</stringProp>
<boolProp name="TestPlan.functional_mode">false</boolProp>
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
<elementProp name="TestPlan.arguments" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
<collectionProp name="Arguments.arguments">
<elementProp name="BASE_URL" elementType="Argument">
<stringProp name="Argument.name">BASE_URL</stringProp>
<stringProp name="Argument.value">http://localhost:8080</stringProp>
</elementProp>
</collectionProp>
</elementProp>
</TestPlan>
<hashTree>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="User API Load Test">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
<boolProp name="LoopController.continue_forever">false</boolProp>
<stringProp name="LoopController.loops">10</stringProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">50</stringProp>
<stringProp name="ThreadGroup.ramp_time">30</stringProp>
<boolProp name="ThreadGroup.scheduler">false</boolProp>
</ThreadGroup>
</hashTree>
</hashTree>
</jmeterTestPlan>
压力测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PerformanceTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
@DisplayName("并发用户创建性能测试")
void concurrentUserCreationTest() throws InterruptedException {
int numberOfThreads = 10;
int requestsPerThread = 100;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
long startTime = System.currentTimeMillis();
for (int i = 0; i < numberOfThreads; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < requestsPerThread; j++) {
CreateUserRequest request = CreateUserRequest.builder()
.username("user_" + threadId + "_" + j)
.email("user_" + threadId + "_" + j + "@example.com")
.password("password123")
.build();
try {
ResponseEntity<ApiResponse<UserResponse>> response =
restTemplate.postForEntity("/api/users", request,
new ParameterizedTypeReference<ApiResponse<UserResponse>>() {});
if (response.getStatusCode().is2xxSuccessful()) {
successCount.incrementAndGet();
} else {
errorCount.incrementAndGet();
}
} catch (Exception e) {
errorCount.incrementAndGet();
}
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
long totalTime = endTime - startTime;
int totalRequests = numberOfThreads * requestsPerThread;
double throughput = (double) totalRequests / (totalTime / 1000.0);
System.out.println("Performance Test Results:");
System.out.println("Total Requests: " + totalRequests);
System.out.println("Successful Requests: " + successCount.get());
System.out.println("Failed Requests: " + errorCount.get());
System.out.println("Total Time: " + totalTime + " ms");
System.out.println("Throughput: " + String.format("%.2f", throughput) + " requests/second");
// 断言性能要求
assertThat(throughput).isGreaterThan(50.0); // 至少50 RPS
assertThat((double) successCount.get() / totalRequests).isGreaterThan(0.95); // 95%成功率
}
}
6.5 调试技巧
日志配置
# application-dev.properties
# 根日志级别
logging.level.root=INFO
# 应用日志级别
logging.level.com.example.myapp=DEBUG
# Spring框架日志
logging.level.org.springframework.web=DEBUG
logging.level.org.springframework.security=DEBUG
# 数据库相关日志
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
logging.level.org.springframework.jdbc.core=DEBUG
# 日志文件配置
logging.file.name=logs/application.log
logging.file.max-size=10MB
logging.file.max-history=30
# 日志格式
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
自定义日志配置
<!-- logback-spring.xml -->
<configuration>
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{36}:%line] - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{36}:%line] - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.myapp" level="DEBUG"/>
<logger name="org.springframework.web" level="DEBUG"/>
<logger name="org.hibernate.SQL" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
<springProfile name="prod">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level [%logger{36}] - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.example.myapp" level="INFO"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<root level="WARN">
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>
调试工具类
@Component
@Slf4j
public class DebugUtils {
// 方法执行时间监控
public static <T> T timeExecution(String operationName, Supplier<T> operation) {
long startTime = System.currentTimeMillis();
try {
T result = operation.get();
long endTime = System.currentTimeMillis();
log.debug("Operation '{}' completed in {} ms", operationName, endTime - startTime);
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("Operation '{}' failed after {} ms", operationName, endTime - startTime, e);
throw e;
}
}
// 内存使用监控
public static void logMemoryUsage(String context) {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;
long maxMemory = runtime.maxMemory();
log.debug("Memory usage at {}: Used={} MB, Free={} MB, Total={} MB, Max={} MB",
context,
usedMemory / 1024 / 1024,
freeMemory / 1024 / 1024,
totalMemory / 1024 / 1024,
maxMemory / 1024 / 1024);
}
// 请求追踪
public static void logRequestDetails(HttpServletRequest request) {
log.debug("Request Details: Method={}, URI={}, RemoteAddr={}, UserAgent={}",
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr(),
request.getHeader("User-Agent"));
// 记录请求参数
Map<String, String[]> parameterMap = request.getParameterMap();
if (!parameterMap.isEmpty()) {
log.debug("Request Parameters: {}", parameterMap);
}
}
// 数据库查询监控
@EventListener
public void handleQueryExecution(QueryExecutionEvent event) {
if (event.getExecutionTime() > 1000) { // 超过1秒的查询
log.warn("Slow query detected: {} ms - {}",
event.getExecutionTime(),
event.getSql());
}
}
}
// 性能监控切面
@Aspect
@Component
@Slf4j
public class PerformanceMonitoringAspect {
@Around("@annotation(Monitored)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
return result;
} catch (Exception e) {
log.error("Exception in {}.{}: {}", className, methodName, e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
log.debug("Performance: {}.{} executed in {} ms",
className, methodName, executionTime);
if (executionTime > 5000) {
log.warn("Slow method detected: {}.{} took {} ms",
className, methodName, executionTime);
}
}
}
}
// 监控注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
}
远程调试配置
# 启动应用时添加JVM参数
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar myapp.jar
# 或者在IDE中配置远程调试
# IntelliJ IDEA: Run -> Edit Configurations -> + -> Remote JVM Debug
# Host: localhost
# Port: 5005
健康检查和监控
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Autowired
private UserService userService;
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try {
// 检查数据库连接
try (Connection connection = dataSource.getConnection()) {
if (!connection.isValid(5)) {
return Health.down()
.withDetail("database", "Connection validation failed")
.build();
}
}
// 检查关键服务
long userCount = userService.getTotalUserCount();
return Health.up()
.withDetail("database", "Connected")
.withDetail("userCount", userCount)
.withDetail("timestamp", LocalDateTime.now())
.build();
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}
// 自定义指标
@Component
public class CustomMetrics {
private final Counter userCreationCounter;
private final Timer userCreationTimer;
private final Gauge activeUsersGauge;
@Autowired
private UserService userService;
public CustomMetrics(MeterRegistry meterRegistry) {
this.userCreationCounter = Counter.builder("user.creation.count")
.description("Number of users created")
.register(meterRegistry);
this.userCreationTimer = Timer.builder("user.creation.time")
.description("Time taken to create a user")
.register(meterRegistry);
this.activeUsersGauge = Gauge.builder("user.active.count")
.description("Number of active users")
.register(meterRegistry, this, CustomMetrics::getActiveUserCount);
}
public void incrementUserCreation() {
userCreationCounter.increment();
}
public Timer.Sample startUserCreationTimer() {
return Timer.start();
}
public void stopUserCreationTimer(Timer.Sample sample) {
sample.stop(userCreationTimer);
}
private double getActiveUserCount() {
return userService.getActiveUserCount();
}
}
总结
本章详细介绍了Spring Boot测试与调试的核心内容:
- 测试框架概述:Spring Boot测试特性、依赖配置、测试配置
- 单元测试:Service层测试、Repository层测试、参数化测试
- 集成测试:Web层测试、MockMvc测试、Testcontainers使用
- 性能测试:JMeter测试计划、压力测试、并发测试
- 调试技巧:日志配置、调试工具类、性能监控、远程调试
- 监控和健康检查:自定义健康指标、性能指标、应用监控
掌握这些测试和调试技能,可以确保Spring Boot应用的质量和稳定性,提高开发效率和问题排查能力。