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测试与调试的核心内容:

  1. 测试框架概述:Spring Boot测试特性、依赖配置、测试配置
  2. 单元测试:Service层测试、Repository层测试、参数化测试
  3. 集成测试:Web层测试、MockMvc测试、Testcontainers使用
  4. 性能测试:JMeter测试计划、压力测试、并发测试
  5. 调试技巧:日志配置、调试工具类、性能监控、远程调试
  6. 监控和健康检查:自定义健康指标、性能指标、应用监控

掌握这些测试和调试技能,可以确保Spring Boot应用的质量和稳定性,提高开发效率和问题排查能力。